From 678eb6a762637c9f68eddb133d452a5f8d7600d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 09:47:01 -0700 Subject: [PATCH 1/4] chore: bump distroless/base from `fb282f8` to `a557d78` in /docker (#723) Bumps distroless/base from `fb282f8` to `a557d78`. --- updated-dependencies: - dependency-name: distroless/base dependency-version: nonroot dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/hub-agent.Dockerfile | 2 +- docker/member-agent.Dockerfile | 2 +- docker/refresh-token.Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/hub-agent.Dockerfile b/docker/hub-agent.Dockerfile index db973b1c2..462c1e595 100644 --- a/docker/hub-agent.Dockerfile +++ b/docker/hub-agent.Dockerfile @@ -23,7 +23,7 @@ RUN CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH GOEXPERIMENT=systemcrypto GO111MODUL # Use distroless as minimal base image to package the hubagent binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/base:nonroot@sha256:fb282f8ed3057f71dbfe3ea0f5fa7e961415dafe4761c23948a9d4628c6166fe +FROM gcr.io/distroless/base:nonroot@sha256:a557d784ac275c287d2bdf3172f47bece8d2a0ef3c0fdefb712e95084a04a562 WORKDIR / COPY --from=builder /workspace/hubagent . USER 65532:65532 diff --git a/docker/member-agent.Dockerfile b/docker/member-agent.Dockerfile index 806d18e41..d5ea7c0b8 100644 --- a/docker/member-agent.Dockerfile +++ b/docker/member-agent.Dockerfile @@ -23,7 +23,7 @@ RUN CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH GOEXPERIMENT=systemcrypto GO111MODUL # Use distroless as minimal base image to package the memberagent binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/base:nonroot@sha256:fb282f8ed3057f71dbfe3ea0f5fa7e961415dafe4761c23948a9d4628c6166fe +FROM gcr.io/distroless/base:nonroot@sha256:a557d784ac275c287d2bdf3172f47bece8d2a0ef3c0fdefb712e95084a04a562 WORKDIR / COPY --from=builder /workspace/memberagent . USER 65532:65532 diff --git a/docker/refresh-token.Dockerfile b/docker/refresh-token.Dockerfile index 2d9972b70..3962de196 100644 --- a/docker/refresh-token.Dockerfile +++ b/docker/refresh-token.Dockerfile @@ -26,7 +26,7 @@ RUN CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH GOEXPERIMENT=systemcrypto GO111MODUL # Use distroless as minimal base image to package the refreshtoken binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/base:nonroot@sha256:fb282f8ed3057f71dbfe3ea0f5fa7e961415dafe4761c23948a9d4628c6166fe +FROM gcr.io/distroless/base:nonroot@sha256:a557d784ac275c287d2bdf3172f47bece8d2a0ef3c0fdefb712e95084a04a562 WORKDIR / COPY --from=builder /workspace/refreshtoken . USER 65532:65532 From 97118e778212c57e3de606ca00abf5009d4f069f Mon Sep 17 00:00:00 2001 From: Britania Rodriguez Reyes <145056127+britaniar@users.noreply.github.com> Date: Fri, 29 May 2026 11:45:46 -0700 Subject: [PATCH 2/4] fix: fix subsequent failure metrics and revert some changes (#717) --- pkg/controllers/updaterun/controller.go | 7 +- pkg/controllers/updaterun/initialization.go | 24 +-- pkg/controllers/updaterun/metrics.go | 54 +++++-- pkg/controllers/updaterun/metrics_test.go | 157 ++++++++++++++++++-- 4 files changed, 200 insertions(+), 42 deletions(-) diff --git a/pkg/controllers/updaterun/controller.go b/pkg/controllers/updaterun/controller.go index baf672168..c551fa528 100644 --- a/pkg/controllers/updaterun/controller.go +++ b/pkg/controllers/updaterun/controller.go @@ -109,17 +109,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req runtime.Request) (runtim return runtime.Result{}, err } - // Track errors for metrics emission. The error is used to determine the failure type - // (user_error vs internal_error) in the emitted metrics. - var reconcileErr error // Emit the update run status metric based on status conditions in the updateRun. - // Use a closure to capture reconcileErr by reference, so it reflects any updates made during reconciliation. - defer func() { emitUpdateRunStatusMetric(updateRun, reconcileErr) }() + defer emitUpdateRunStatusMetric(updateRun) state := updateRun.GetUpdateRunSpec().State var updatingStageIndex int var toBeUpdatedBindings, toBeDeletedBindings []placementv1beta1.BindingObj + var reconcileErr error updateRunStatus := updateRun.GetUpdateRunStatus() initCond := meta.FindStatusCondition(updateRunStatus.Conditions, string(placementv1beta1.StagedUpdateRunConditionInitialized)) if !condition.IsConditionStatusTrue(initCond, updateRun.GetGeneration()) { diff --git a/pkg/controllers/updaterun/initialization.go b/pkg/controllers/updaterun/initialization.go index 4eca2b1d9..7a3217462 100644 --- a/pkg/controllers/updaterun/initialization.go +++ b/pkg/controllers/updaterun/initialization.go @@ -91,7 +91,7 @@ func (r *Reconciler) validatePlacement(ctx context.Context, updateRun placementv if apierrors.IsNotFound(err) { placementNotFoundErr := controller.NewUserError(fmt.Errorf("parent placement not found")) klog.ErrorS(err, "Failed to get placement", "placement", placementKey, "updateRun", updateRunRef) - return nil, types.NamespacedName{}, fmt.Errorf("%w: %w", errValidationFailed, placementNotFoundErr) + return nil, types.NamespacedName{}, fmt.Errorf("%w: %s", errValidationFailed, placementNotFoundErr.Error()) } klog.ErrorS(err, "Failed to get placement", "placement", placementKey, "updateRun", updateRunRef) return nil, types.NamespacedName{}, controller.NewAPIServerError(true, err) @@ -106,7 +106,7 @@ func (r *Reconciler) validatePlacement(ctx context.Context, updateRun placementv if placementSpec.Strategy.Type != placementv1beta1.ExternalRolloutStrategyType { klog.V(2).InfoS("The placement does not have an external rollout strategy", "placement", placementKey, "updateRun", updateRunRef) wrongRolloutTypeErr := controller.NewUserError(errors.New("parent placement does not have an external rollout strategy, current strategy: " + string(placementSpec.Strategy.Type))) - return nil, types.NamespacedName{}, fmt.Errorf("%w: %w", errValidationFailed, wrongRolloutTypeErr) + return nil, types.NamespacedName{}, fmt.Errorf("%w: %s", errValidationFailed, wrongRolloutTypeErr.Error()) } updateRunStatus := updateRun.GetUpdateRunStatus() @@ -271,7 +271,7 @@ func (r *Reconciler) generateStagesByStrategy( if apierrors.IsNotFound(err) { // we won't continue or retry the initialization if the UpdateStrategy is not found. strategyNotFoundErr := controller.NewUserError(fmt.Errorf("referenced updateStrategy not found: `%s`", strategyKey)) - return fmt.Errorf("%w: %w", errValidationFailed, strategyNotFoundErr) + return fmt.Errorf("%w: %s", errValidationFailed, strategyNotFoundErr.Error()) } // other err can be retried. return controller.NewAPIServerError(true, err) @@ -338,13 +338,13 @@ func (r *Reconciler) computeRunStageStatus( klog.ErrorS(err, "Failed to validate the before stage tasks", "updateStrategy", strategyKey, "stageName", stage.Name, "updateRun", updateRunRef) // no more retries here. invalidBeforeStageErr := controller.NewUserError(fmt.Errorf("the before stage tasks are invalid, updateStrategy: `%s`, stage: %s, err: %s", strategyKey, stage.Name, err.Error())) - return fmt.Errorf("%w: %w", errValidationFailed, invalidBeforeStageErr) + return fmt.Errorf("%w: %s", errValidationFailed, invalidBeforeStageErr.Error()) } if err := validateAfterStageTask(stage.AfterStageTasks); err != nil { klog.ErrorS(err, "Failed to validate the after stage tasks", "updateStrategy", strategyKey, "stageName", stage.Name, "updateRun", updateRunRef) // no more retries here. invalidAfterStageErr := controller.NewUserError(fmt.Errorf("the after stage tasks are invalid, updateStrategy: `%s`, stage: %s, err: %s", strategyKey, stage.Name, err.Error())) - return fmt.Errorf("%w: %w", errValidationFailed, invalidAfterStageErr) + return fmt.Errorf("%w: %s", errValidationFailed, invalidAfterStageErr.Error()) } curStageUpdatingStatus := placementv1beta1.StageUpdatingStatus{StageName: stage.Name} @@ -354,7 +354,7 @@ func (r *Reconciler) computeRunStageStatus( klog.ErrorS(err, "Failed to convert label selector", "updateStrategy", strategyKey, "stageName", stage.Name, "labelSelector", stage.LabelSelector, "updateRun", updateRunRef) // no more retries here. invalidLabelErr := controller.NewUserError(fmt.Errorf("the stage label selector is invalid, updateStrategy: `%s`, stage: %s, err: %s", strategyKey, stage.Name, err.Error())) - return fmt.Errorf("%w: %w", errValidationFailed, invalidLabelErr) + return fmt.Errorf("%w: %s", errValidationFailed, invalidLabelErr.Error()) } // List all the clusters that match the label selector. var clusterList clusterv1beta1.MemberClusterList @@ -372,7 +372,7 @@ func (r *Reconciler) computeRunStageStatus( dupErr := controller.NewUserError(fmt.Errorf("cluster `%s` appears in more than one stages", cluster.Name)) klog.ErrorS(dupErr, "Failed to compute the stage", "updateStrategy", strategyKey, "stageName", stage.Name, "updateRun", updateRunRef) // no more retries here. - return fmt.Errorf("%w: %w", errValidationFailed, dupErr) + return fmt.Errorf("%w: %s", errValidationFailed, dupErr.Error()) } if stage.SortingLabelKey != nil { // interpret the label values as integers. @@ -380,7 +380,7 @@ func (r *Reconciler) computeRunStageStatus( keyErr := controller.NewUserError(fmt.Errorf("the sorting label `%s:%s` on cluster `%s` is not valid: %s", *stage.SortingLabelKey, cluster.Labels[*stage.SortingLabelKey], cluster.Name, err.Error())) klog.ErrorS(keyErr, "Failed to sort clusters in the stage", "updateStrategy", strategyKey, "stageName", stage.Name, "updateRun", updateRunRef) // no more retries here. - return fmt.Errorf("%w: %w", errValidationFailed, keyErr) + return fmt.Errorf("%w: %s", errValidationFailed, keyErr.Error()) } } curStageClusters = append(curStageClusters, cluster) @@ -447,7 +447,7 @@ func (r *Reconciler) computeRunStageStatus( sort.Strings(missingClusters) klog.ErrorS(missingErr, "Clusters are missing in any stage", "clusters", strings.Join(missingClusters, ", "), "updateStrategy", strategyKey, "updateRun", updateRunRef) // no more retries here, only show the first 10 missing clusters in the placement status. - return fmt.Errorf("%w: %w, total %d, showing up to 10: %s", errValidationFailed, missingErr, len(missingClusters), strings.Join(missingClusters[:min(10, len(missingClusters))], ", ")) + return fmt.Errorf("%w: %s, total %d, showing up to 10: %s", errValidationFailed, missingErr.Error(), len(missingClusters), strings.Join(missingClusters[:min(10, len(missingClusters))], ", ")) } return nil } @@ -564,7 +564,7 @@ func (r *Reconciler) getResourceSnapshotObjs(ctx context.Context, placement plac userErr := controller.NewUserError(fmt.Errorf("invalid resource snapshot index `%s` provided, expected an integer >= 0", updateRunSpec.ResourceSnapshotIndex)) klog.ErrorS(userErr, "Failed to parse the resource snapshot index", "updateRun", updateRunRef) // no more retries here. - return nil, fmt.Errorf("%w: %w", errValidationFailed, userErr) + return nil, fmt.Errorf("%w: %s", errValidationFailed, userErr.Error()) } resourceSnapshotList, err := controller.ListAllResourceSnapshotWithAnIndex(ctx, r.Client, updateRunSpec.ResourceSnapshotIndex, placementKey.Name, placementKey.Namespace) @@ -580,7 +580,7 @@ func (r *Reconciler) getResourceSnapshotObjs(ctx context.Context, placement plac userErr := controller.NewUserError(fmt.Errorf("no resourceSnapshots with index `%d` found for placement `%s`", snapshotIndex, placementKey)) klog.ErrorS(userErr, "No specified resourceSnapshots found", "updateRun", updateRunRef) // no more retries here. - return resourceSnapshotObjs, fmt.Errorf("%w: %w", errValidationFailed, userErr) + return resourceSnapshotObjs, fmt.Errorf("%w: %s", errValidationFailed, userErr.Error()) } return resourceSnapshotObjs, nil } @@ -592,7 +592,7 @@ func (r *Reconciler) getResourceSnapshotObjs(ctx context.Context, placement plac if err != nil { klog.ErrorS(err, "Failed to select resources for placement", "placement", placementKey, "updateRun", updateRunRef) if errors.Is(err, controller.ErrUserError) { - return nil, fmt.Errorf("%w: %w", errValidationFailed, err) + return nil, fmt.Errorf("%w: %s", errValidationFailed, err.Error()) } return nil, err } diff --git a/pkg/controllers/updaterun/metrics.go b/pkg/controllers/updaterun/metrics.go index 6c2ab6448..1012bcb23 100644 --- a/pkg/controllers/updaterun/metrics.go +++ b/pkg/controllers/updaterun/metrics.go @@ -17,11 +17,12 @@ limitations under the License. package updaterun import ( - "errors" + "strings" "time" "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" @@ -37,35 +38,58 @@ func deleteUpdateRunMetrics(updateRun placementv1beta1.UpdateRunObj) { hubmetrics.FleetUpdateRunApprovalRequestLatencySeconds.DeletePartialMatch(prometheus.Labels{"namespace": updateRun.GetNamespace(), "name": updateRun.GetName()}) } -// determineFailureType determines the type of failure based on the condition status and error. +// determineFailureType determines the type of failure based on the condition. // It returns: -// - "none" for successful, in-progress, waiting, or stopping conditions -// - "user_error" for known customer configuration errors (when err wraps controller.ErrUserError) -// - "internal_error" for errors that require investigation -func determineFailureType(err error) hubmetrics.UpdateRunFailureType { - if err != nil { - // Check if error for the failed condition is a user error. - if errors.Is(err, controller.ErrUserError) { +// - "none" for successful, in-progress, waiting, stopping (unknown status), or stopped conditions +// - "user_error" for known customer configuration errors (when the condition message +// contains the user error marker) +// - "internal_error" for terminal failure conditions or stuck conditions that require investigation +// +// The failure type is derived from the condition message to ensure consistency across +// reconciliations. This handles subsequent reconciliations where the original error +// is not available but the failure type needs to be preserved in the condition message. +// +// Note: A "stuck" update run is classified as "internal_error" because it may never resolve +// and effectively represents a failure state that needs investigation. +func determineFailureType(cond *metav1.Condition) hubmetrics.UpdateRunFailureType { + if cond == nil || cond.Status != metav1.ConditionFalse { + return hubmetrics.UpdateRunFailureTypeNone + } + + // Stuck is always classified as internal error + if cond.Reason == condition.UpdateRunStuckReason { + return hubmetrics.UpdateRunFailureTypeInternalError + } + + // Check if it's a terminal failure condition + if isFailureReason(cond.Reason) { + if strings.Contains(cond.Message, controller.ErrUserError.Error()) { return hubmetrics.UpdateRunFailureTypeUserError } - // Failed condition that is not a user error is an internal error. return hubmetrics.UpdateRunFailureTypeInternalError } - // If there's no error, there's no failure to categorize. return hubmetrics.UpdateRunFailureTypeNone } +// isFailureReason returns true if the condition reason indicates a terminal failure state +// for an UpdateRun. Non-failure reasons like stuck, waiting, stopping, or stopped are +// not considered terminal failures. +func isFailureReason(reason string) bool { + return reason == condition.UpdateRunFailedReason || + reason == condition.UpdateRunInitializeFailedReason +} + // emitUpdateRunStatusMetric emits the update run status metric based on status conditions in the updateRun. -// The err parameter is used to determine the failure type for failed conditions. -func emitUpdateRunStatusMetric(updateRun placementv1beta1.UpdateRunObj, err error) { +// The failure type is derived from the condition message, not from the reconcile error. +func emitUpdateRunStatusMetric(updateRun placementv1beta1.UpdateRunObj) { generation := updateRun.GetGeneration() state := updateRun.GetUpdateRunSpec().State updateRunStatus := updateRun.GetUpdateRunStatus() - failureType := determineFailureType(err) succeedCond := meta.FindStatusCondition(updateRunStatus.Conditions, string(placementv1beta1.StagedUpdateRunConditionSucceeded)) if succeedCond != nil && succeedCond.ObservedGeneration == generation { + failureType := determineFailureType(succeedCond) hubmetrics.FleetUpdateRunStatusLastTimestampSeconds.WithLabelValues(updateRun.GetNamespace(), updateRun.GetName(), string(state), string(placementv1beta1.StagedUpdateRunConditionSucceeded), string(succeedCond.Status), succeedCond.Reason, string(failureType)).SetToCurrentTime() return @@ -73,6 +97,7 @@ func emitUpdateRunStatusMetric(updateRun placementv1beta1.UpdateRunObj, err erro progressingCond := meta.FindStatusCondition(updateRunStatus.Conditions, string(placementv1beta1.StagedUpdateRunConditionProgressing)) if progressingCond != nil && progressingCond.ObservedGeneration == generation { + failureType := determineFailureType(progressingCond) hubmetrics.FleetUpdateRunStatusLastTimestampSeconds.WithLabelValues(updateRun.GetNamespace(), updateRun.GetName(), string(state), string(placementv1beta1.StagedUpdateRunConditionProgressing), string(progressingCond.Status), progressingCond.Reason, string(failureType)).SetToCurrentTime() return @@ -80,6 +105,7 @@ func emitUpdateRunStatusMetric(updateRun placementv1beta1.UpdateRunObj, err erro initializedCond := meta.FindStatusCondition(updateRunStatus.Conditions, string(placementv1beta1.StagedUpdateRunConditionInitialized)) if initializedCond != nil && initializedCond.ObservedGeneration == generation { + failureType := determineFailureType(initializedCond) hubmetrics.FleetUpdateRunStatusLastTimestampSeconds.WithLabelValues(updateRun.GetNamespace(), updateRun.GetName(), string(state), string(placementv1beta1.StagedUpdateRunConditionInitialized), string(initializedCond.Status), initializedCond.Reason, string(failureType)).SetToCurrentTime() return diff --git a/pkg/controllers/updaterun/metrics_test.go b/pkg/controllers/updaterun/metrics_test.go index 2e0139c0e..a1bb15321 100644 --- a/pkg/controllers/updaterun/metrics_test.go +++ b/pkg/controllers/updaterun/metrics_test.go @@ -17,50 +17,185 @@ limitations under the License. package updaterun import ( - "errors" "fmt" "testing" "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" hubmetrics "github.com/kubefleet-dev/kubefleet/pkg/metrics/hub" + "github.com/kubefleet-dev/kubefleet/pkg/utils/condition" "github.com/kubefleet-dev/kubefleet/pkg/utils/controller" ) func TestDetermineFailureType(t *testing.T) { tests := []struct { name string - err error + cond *metav1.Condition want hubmetrics.UpdateRunFailureType }{ { - name: "update run no failure", - err: nil, + name: "nil condition", + cond: nil, want: hubmetrics.UpdateRunFailureTypeNone, }, { - name: "update run failed with user error", - err: fmt.Errorf("cannot continue the updateRun: failed to validate the updateRun: %w", controller.ErrUserError), + name: "succeeded condition status is true", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionSucceeded), + Status: metav1.ConditionTrue, + Reason: condition.UpdateRunSucceededReason, + Message: "update run succeeded", + }, + want: hubmetrics.UpdateRunFailureTypeNone, + }, + { + name: "progressing condition is false with reason stuck - internal error", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionProgressing), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunStuckReason, + Message: "updateRun is stuck waiting for 1 cluster(s) in stage stage1 to finish updating", + }, + want: hubmetrics.UpdateRunFailureTypeInternalError, + }, + { + name: "progressing condition is false but reason is waiting - not a failure", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionProgressing), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunWaitingReason, + Message: "waiting for approval", + }, + want: hubmetrics.UpdateRunFailureTypeNone, + }, + { + name: "progressing condition is unknown but reason is stopping - not a failure", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionProgressing), + Status: metav1.ConditionUnknown, + Reason: condition.UpdateRunStoppingReason, + Message: "stopping the update run", + }, + want: hubmetrics.UpdateRunFailureTypeNone, + }, + { + name: "progressing condition is false but reason is stopped - not a failure", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionProgressing), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunStoppedReason, + Message: "update run has been stopped", + }, + want: hubmetrics.UpdateRunFailureTypeNone, + }, + { + name: "succeeded condition is false with UpdateRunFailed reason and user error in message", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionSucceeded), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunFailedReason, + Message: controller.NewUserError(fmt.Errorf("invalid CRP selector")).Error(), + }, want: hubmetrics.UpdateRunFailureTypeUserError, }, { - name: "update run failed with internal error", - err: errors.New("cannot continue the updateRun"), + name: "succeeded condition is false with UpdateRunFailed reason but no user error in message", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionSucceeded), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunFailedReason, + Message: "cannot continue the updateRun: some internal error", + }, + want: hubmetrics.UpdateRunFailureTypeInternalError, + }, + { + name: "progressing condition is false with unexpected error in message", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionProgressing), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunFailedReason, + Message: controller.NewUnexpectedBehaviorError(fmt.Errorf("found unsupported task type in before stage tasks: %s", v1beta1.StageTaskTypeTimedWait)).Error(), + }, want: hubmetrics.UpdateRunFailureTypeInternalError, }, { - name: "update run is stuck - internal error", - err: errors.New("updateRun is stuck waiting for 1 cluster(s) in stage stage1 to finish updating, please check placement status for potential errors"), + name: "initialized condition is false with UpdateRunInitializeFailed reason but no user error in message", + cond: &metav1.Condition{ + Type: string(v1beta1.StagedUpdateRunConditionInitialized), + Status: metav1.ConditionFalse, + Reason: condition.UpdateRunInitializeFailedReason, + Message: "failed to initialize: internal error", + }, want: hubmetrics.UpdateRunFailureTypeInternalError, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - got := determineFailureType(tc.err) + got := determineFailureType(tc.cond) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("determineFailureType() = %v, want %v, diff (-want +got):\n%s", got, tc.want, diff) } }) } } + +func TestIsFailureReason(t *testing.T) { + tests := []struct { + name string + reason string + want bool + }{ + { + name: "UpdateRunFailedReason is a failure", + reason: condition.UpdateRunFailedReason, + want: true, + }, + { + name: "UpdateRunInitializeFailedReason is a failure", + reason: condition.UpdateRunInitializeFailedReason, + want: true, + }, + { + name: "UpdateRunStuckReason is not a terminal failure", + reason: condition.UpdateRunStuckReason, + want: false, + }, + { + name: "UpdateRunWaitingReason is not a failure", + reason: condition.UpdateRunWaitingReason, + want: false, + }, + { + name: "UpdateRunStoppingReason is not a failure", + reason: condition.UpdateRunStoppingReason, + want: false, + }, + { + name: "UpdateRunStoppedReason is not a failure", + reason: condition.UpdateRunStoppedReason, + want: false, + }, + { + name: "UpdateRunSucceededReason is not a failure", + reason: condition.UpdateRunSucceededReason, + want: false, + }, + { + name: "UpdateRunProgressingReason is not a failure", + reason: condition.UpdateRunProgressingReason, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := isFailureReason(tc.reason) + if got != tc.want { + t.Errorf("isFailureReason(%q) = %v, want %v", tc.reason, got, tc.want) + } + }) + } +} From 660c868804eedcee3b9827a3e74936c1eb819814 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Mon, 1 Jun 2026 13:52:52 +1000 Subject: [PATCH 3/4] feat: migrating to validating admission policies for our validating logic (2/): enable the VAP manager (#729) --- charts/hub-agent/README.md | 70 +++--- charts/hub-agent/templates/config.yaml | 11 + charts/hub-agent/templates/deployment.yaml | 22 ++ charts/hub-agent/templates/rbac.yaml | 8 + charts/hub-agent/values.yaml | 6 + cmd/hubagent/main.go | 48 +++- cmd/hubagent/options/options.go | 6 +- cmd/hubagent/options/options_test.go | 29 ++- cmd/hubagent/options/validation.go | 41 +++- cmd/hubagent/options/validation_test.go | 128 +++++++++-- cmd/hubagent/options/webhooks.go | 43 +++- cmd/hubagent/workload/setup.go | 4 +- pkg/admissionpolicymanager/configs.go | 50 ++++ pkg/admissionpolicymanager/manager.go | 21 +- .../manager_integration_test.go | 2 +- pkg/admissionpolicymanager/suite_test.go | 6 +- .../svcaccountsntokenreqs.go | 16 +- pkg/webhook/webhook.go | 16 +- pkg/webhook/webhook_test.go | 4 +- test/e2e/admission_policies_test.go | 217 ++++++++++++++++++ test/e2e/admission_policy_manager_cfg.yaml | 4 + test/e2e/fleet_guard_rail_test.go | 5 + test/e2e/setup.sh | 3 + test/e2e/setup_test.go | 26 +++ 24 files changed, 679 insertions(+), 107 deletions(-) create mode 100644 charts/hub-agent/templates/config.yaml create mode 100644 test/e2e/admission_policies_test.go create mode 100644 test/e2e/admission_policy_manager_cfg.yaml diff --git a/charts/hub-agent/README.md b/charts/hub-agent/README.md index 9582dda49..7cc93551b 100644 --- a/charts/hub-agent/README.md +++ b/charts/hub-agent/README.md @@ -89,39 +89,43 @@ _See [helm install](https://helm.sh/docs/helm/helm_install/) for command documen ## Parameters -| Parameter | Description | Default | -|:------------------------------------------|:------------------------------------------------------------------------------------------|:-------------------------------------------------| -| `replicaCount` | Number of hub-agent replicas to deploy | `1` | -| `image.repository` | Image repository | `ghcr.io/kubefleet-dev/kubefleet/hub-agent` | -| `image.pullPolicy` | Image pull policy | `Always` | -| `image.tag` | Image release tag (empty uses chart `appVersion`) | `""` | -| `namespace` | Namespace where this chart is installed | `fleet-system` | -| `resources` | Resource requests/limits for the container | limits: 500m CPU, 1Gi; requests: 100m CPU, 128Mi | -| `affinity` | Node affinity for hub-agent pods | `{}` | -| `tolerations` | Tolerations for hub-agent pods | `[]` | -| `logVerbosity` | Log level (klog V logs) | `5` | -| `enableWebhook` | Enable webhook server | `true` | -| `webhookServiceName` | Webhook service name | `fleetwebhook` | -| `enableGuardRail` | Enable guard rail webhook configurations | `true` | -| `webhookClientConnectionType` | Connection type for webhook client (service or url) | `service` | -| `useCertManager` | Use cert-manager for webhook certificate management (requires `enableWorkload=true`) | `false` | -| `webhookCertSecretName` | Name of the Secret where cert-manager stores the certificate (required when enabled) | `unset` | -| `enableClusterInventoryAPI` | Enable cluster inventory APIs | `true` | -| `enableStagedUpdateRunAPIs` | Enable staged update run APIs | `true` | -| `enableEvictionAPIs` | Enable eviction APIs | `true` | -| `enablePprof` | Enable pprof endpoint | `true` | -| `pprofPort` | pprof server port | `6065` | -| `hubAPIQPS` | QPS for fleet-apiserver (not including events/node heartbeat) | `250` | -| `hubAPIBurst` | Burst for fleet-apiserver (not including events/node heartbeat) | `1000` | -| `MaxConcurrentClusterPlacement` | Max concurrent ClusterResourcePlacement operations | `100` | -| `ConcurrentResourceChangeSyncs` | Max concurrent resourceChange reconcilers | `20` | -| `logFileMaxSize` | Max log file size before rotation (optional) | `unset` | -| `MaxFleetSizeSupported` | Max number of member clusters supported | `100` | -| `forceDeleteWaitTime` | Grace period before force-deleting resources | `15m0s` | -| `clusterUnhealthyThreshold` | Threshold duration for marking a cluster unhealthy | `3m0s` | -| `resourceSnapshotCreationMinimumInterval` | The minimum interval at which resource snapshots could be created. | `30s` | -| `resourceChangesCollectionDuration` | The duration for collecting resource changes into one snapshot. | `15s` | -| `enableWorkload` | Enable kubernetes builtin workload to run in hub cluster. | `false` | +| Parameter | Description | Default | +|:----------|:------------|:--------| +| `replicaCount` | Number of hub-agent replicas to deploy | `1` | +| `image.repository` | Image repository | `ghcr.io/kubefleet-dev/kubefleet/hub-agent` | +| `image.pullPolicy` | Image pull policy | `Always` | +| `image.tag` | Image release tag (empty uses chart `appVersion`) | `""` | +| `namespace` | Namespace where this chart is installed | `fleet-system` | +| `resources` | Resource requests/limits for the container | limits: 500m CPU, 1Gi; requests: 100m CPU, 128Mi | +| `affinity` | Node affinity for hub-agent pods | `{}` | +| `tolerations` | Tolerations for hub-agent pods | `[]` | +| `logVerbosity` | Log level (klog V logs) | `5` | +| `enableWebhook` | Enable webhook server | `true` | +| `webhookServiceName` | Webhook service name | `fleetwebhook` | +| `enableGuardRail` | Enable guard rail webhook configurations | `true` | +| `webhookClientConnectionType` | Connection type for webhook client (service or url) | `service` | +| `useCertManager` | Use cert-manager for webhook certificate management (requires `enableWorkload=true`) | `false` | +| `webhookCertSecretName` | Name of the Secret where cert-manager stores the certificate (required when enabled) | `unset` | +| `enableClusterInventoryAPI` | Enable cluster inventory APIs | `true` | +| `enableStagedUpdateRunAPIs` | Enable staged update run APIs | `true` | +| `enableEvictionAPIs` | Enable eviction APIs | `true` | +| `enablePprof` | Enable pprof endpoint | `true` | +| `pprofPort` | pprof server port | `6065` | +| `hubAPIQPS` | QPS for fleet-apiserver (not including events/node heartbeat) | `250` | +| `hubAPIBurst` | Burst for fleet-apiserver (not including events/node heartbeat) | `1000` | +| `MaxConcurrentClusterPlacement` | Max concurrent ClusterResourcePlacement operations | `100` | +| `ConcurrentResourceChangeSyncs` | Max concurrent resourceChange reconcilers | `20` | +| `logFileMaxSize` | Max log file size before rotation (optional) | `unset` | +| `MaxFleetSizeSupported` | Max number of member clusters supported | `100` | +| `forceDeleteWaitTime` | Grace period before force-deleting resources | `15m0s` | +| `clusterUnhealthyThreshold` | Threshold duration for marking a cluster unhealthy | `3m0s` | +| `resourceSnapshotCreationMinimumInterval` | The minimum interval at which resource snapshots could be created | `30s` | +| `resourceChangesCollectionDuration` | The duration for collecting resource changes into one snapshot | `15s` | +| `enableWorkload` | Enable kubernetes builtin workload to run in hub cluster | `false` | +| `additionalConfigData` | Additional key-value data to include in the hub agent config map | `{}` | +| `additionalConfigDataMountPath` | Mount path for the additional config data volume | `/etc/kubefleet/additional-config` | +| `enableAdmissionPolicyManager` | Enable the admission policy manager to enforce VAP-based policies on the hub cluster | `false` | +| `admissionPolicyManagerConfigName` | Name of the key that contains the admission policy manager configuration in the hub agent config map | `""` | ## Certificate Management diff --git a/charts/hub-agent/templates/config.yaml b/charts/hub-agent/templates/config.yaml new file mode 100644 index 000000000..27c2f37e2 --- /dev/null +++ b/charts/hub-agent/templates/config.yaml @@ -0,0 +1,11 @@ +{{- if .Values.additionalConfigData }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "hub-agent.fullname" . }}-config + namespace: {{ .Values.namespace }} + labels: + {{- include "hub-agent.labels" . | nindent 4 }} +data: + {{- .Values.additionalConfigData | toYaml | nindent 2 }} +{{- end }} diff --git a/charts/hub-agent/templates/deployment.yaml b/charts/hub-agent/templates/deployment.yaml index 882a3fa12..57b857298 100644 --- a/charts/hub-agent/templates/deployment.yaml +++ b/charts/hub-agent/templates/deployment.yaml @@ -62,6 +62,13 @@ spec: - --cluster-unhealthy-threshold={{ .Values.clusterUnhealthyThreshold }} - --resource-snapshot-creation-minimum-interval={{ .Values.resourceSnapshotCreationMinimumInterval }} - --resource-changes-collection-duration={{ .Values.resourceChangesCollectionDuration }} + - --enable-admission-policy-manager={{ .Values.enableAdmissionPolicyManager }} + {{- if and .Values.admissionPolicyManagerConfigName (not .Values.additionalConfigData) }} + {{- fail "ERROR: admissionPolicyManagerConfigName is set but additionalConfigData is empty; must provide admission policy manager configuration data" }} + {{- end }} + {{- if and .Values.additionalConfigData .Values.admissionPolicyManagerConfigName }} + - --admission-policy-manager-config={{ .Values.additionalConfigDataMountPath }}/{{ .Values.admissionPolicyManagerConfigName }} + {{- end }} ports: - name: metrics containerPort: 8080 @@ -96,6 +103,11 @@ spec: # This path must match FleetWebhookCertDir in pkg/webhook/webhook.go mountPath: /tmp/k8s-webhook-server/serving-certs readOnly: true + {{- if .Values.additionalConfigData }} + - name: additional-config + mountPath: {{ .Values.additionalConfigDataMountPath }} + readOnly: true + {{- end }} {{- else }} volumeMounts: - name: webhook-cert @@ -104,6 +116,11 @@ spec: # the read only root filesystem setup would block the agent from attempting to # clear the directory. mountPath: /tmp/k8s-webhook-server/ + {{- if .Values.additionalConfigData }} + - name: additional-config + mountPath: {{ .Values.additionalConfigDataMountPath }} + readOnly: true + {{- end }} {{- end }} volumes: - name: webhook-cert @@ -116,6 +133,11 @@ spec: {{- else }} emptyDir: {} {{- end }} + {{- if .Values.additionalConfigData }} + - name: additional-config + configMap: + name: {{ include "hub-agent.fullname" . }}-config + {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} diff --git a/charts/hub-agent/templates/rbac.yaml b/charts/hub-agent/templates/rbac.yaml index 70edffc05..11b58a658 100644 --- a/charts/hub-agent/templates/rbac.yaml +++ b/charts/hub-agent/templates/rbac.yaml @@ -139,6 +139,14 @@ rules: - fleet-validating-webhook-configuration - fleet-guard-rail-webhook-configuration verbs: ["delete"] + + # Admission policies management. If the admission policy manager is enabled, the hub agent + # will install/uninstall validating admission policies and bindings based on user-provided configuration. + {{- if .Values.enableAdmissionPolicyManager }} + - apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingadmissionpolicies", "validatingadmissionpolicybindings"] + verbs: ["get", "list", "create", "update", "patch", "delete"] + {{- end }} # Leader election. Split into two rules so that only the hub-agent's own # election lease can be mutated; a compromised hub-agent cannot hijack diff --git a/charts/hub-agent/values.yaml b/charts/hub-agent/values.yaml index 4964cba84..0db937227 100644 --- a/charts/hub-agent/values.yaml +++ b/charts/hub-agent/values.yaml @@ -57,3 +57,9 @@ hubAPIBurst: 1000 MaxConcurrentClusterPlacement: 100 ConcurrentResourceChangeSyncs: 20 MaxFleetSizeSupported: 100 + +additionalConfigData: {} +additionalConfigDataMountPath: /etc/kubefleet/additional-config + +enableAdmissionPolicyManager: false +admissionPolicyManagerConfigName: "" diff --git a/cmd/hubagent/main.go b/cmd/hubagent/main.go index 96891107d..ec14e40eb 100644 --- a/cmd/hubagent/main.go +++ b/cmd/hubagent/main.go @@ -27,12 +27,14 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/yaml" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/klog/v2" clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -46,6 +48,7 @@ import ( placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" "github.com/kubefleet-dev/kubefleet/cmd/hubagent/options" "github.com/kubefleet-dev/kubefleet/cmd/hubagent/workload" + "github.com/kubefleet-dev/kubefleet/pkg/admissionpolicymanager" mcv1beta1 "github.com/kubefleet-dev/kubefleet/pkg/controllers/membercluster/v1beta1" readiness "github.com/kubefleet-dev/kubefleet/pkg/utils/informer/readiness" "github.com/kubefleet-dev/kubefleet/pkg/utils/validator" @@ -160,7 +163,7 @@ func main() { exitWithErrorFunc() } - klog.V(2).InfoS("starting hubagent") + klog.V(2).InfoS("starting hub agent") if opts.FeatureFlags.EnableV1Beta1APIs { klog.Info("Setting up memberCluster v1beta1 controller") if err = (&mcv1beta1.Reconciler{ @@ -183,7 +186,7 @@ func main() { exitWithErrorFunc() } - if opts.WebhookOpts.EnableWebhooks { + if opts.WebhookAndAdmissionPolicyOpts.EnableWebhooks { // Generate webhook configuration with certificates webhookConfig, err := webhook.NewWebhookConfigFromOptions(mgr, opts, FleetWebhookPort) if err != nil { @@ -208,7 +211,7 @@ func main() { // When using cert-manager, add a readiness check to ensure CA bundles are injected before marking ready. // This prevents the pod from accepting traffic before cert-manager has populated the webhook CA bundles, // which would cause webhook calls to fail. - if opts.WebhookOpts.UseCertManager { + if opts.WebhookAndAdmissionPolicyOpts.UseCertManager { if err := mgr.AddReadyzCheck("cert-manager-ca-injection", func(req *http.Request) error { return webhookConfig.CheckCAInjection(req.Context()) }); err != nil { @@ -220,6 +223,45 @@ func main() { } ctx := ctrl.SetupSignalHandler() + + if opts.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager { + policyManagerCfgs := admissionpolicymanager.DefaultPolicyGeneratorConfigs + if len(opts.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig) != 0 { + // Read user-provided admission policy manager config from given path. + cfgData, err := os.ReadFile(opts.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig) + if err != nil { + klog.ErrorS(err, "failed to read the admission policy manager config file") + exitWithErrorFunc() + } + + policyManagerCfgs = &admissionpolicymanager.PolicyGeneratorConfigs{} + if err := yaml.Unmarshal(cfgData, policyManagerCfgs); err != nil { + klog.ErrorS(err, "failed to unmarshal the admission policy manager config file") + exitWithErrorFunc() + } + + // Note that validation has been performed when the flags are parsed. + } + + // Create a separate client for the admission policy manager to use, as the cached client from + // the controller manager side is not initialized yet at this point. + hubUncachedClient, err := client.New(defaultCfg, client.Options{Scheme: scheme}) + if err != nil { + klog.ErrorS(err, "failed to create uncached client for the admission policy manager") + exitWithErrorFunc() + } + policyMgr, err := admissionpolicymanager.New(hubUncachedClient, policyManagerCfgs) + if err != nil { + klog.ErrorS(err, "failed to create the admission policy manager") + exitWithErrorFunc() + } + + if err := policyMgr.Start(ctx); err != nil { + klog.ErrorS(err, "failed to start the admission policy manager") + exitWithErrorFunc() + } + } + if err := workload.SetupControllers(ctx, &wg, mgr, defaultCfg, opts); err != nil { klog.ErrorS(err, "unable to set up controllers") exitWithErrorFunc() diff --git a/cmd/hubagent/options/options.go b/cmd/hubagent/options/options.go index d75404172..26207013e 100644 --- a/cmd/hubagent/options/options.go +++ b/cmd/hubagent/options/options.go @@ -28,8 +28,8 @@ type Options struct { // Options that concern the setup of the controller manager instance in use by the KubeFleet hub agent. CtrlMgrOpts ControllerManagerOptions - // KubeFleet webhook related options. - WebhookOpts WebhookOptions + // KubeFleet webhook and admission policy related options. + WebhookAndAdmissionPolicyOpts WebhookAndAdmissionPolicyOptions // Feature flags that control the enabling of certain features in the hub agent. FeatureFlags FeatureFlags @@ -48,7 +48,7 @@ func NewOptions() *Options { func (o *Options) AddFlags(flags *flag.FlagSet) { o.LeaderElectionOpts.AddFlags(flags) o.CtrlMgrOpts.AddFlags(flags) - o.WebhookOpts.AddFlags(flags) + o.WebhookAndAdmissionPolicyOpts.AddFlags(flags) o.FeatureFlags.AddFlags(flags) o.ClusterMgmtOpts.AddFlags(flags) o.PlacementMgmtOpts.AddFlags(flags) diff --git a/cmd/hubagent/options/options_test.go b/cmd/hubagent/options/options_test.go index 138dfcf19..bb3231ceb 100644 --- a/cmd/hubagent/options/options_test.go +++ b/cmd/hubagent/options/options_test.go @@ -706,13 +706,14 @@ func TestPlacementManagementOptions(t *testing.T) { } } -// TestWebhookOptions tests the parsing and validation logic of the webhook options defined in WebhookOptions. +// TestWebhookAndAdmissionPolicyOptions tests the parsing and validation logic of the webhook +// options defined in WebhookAndAdmissionPolicyOptions. func TestWebhookOptions(t *testing.T) { testCases := []struct { name string flagSetName string args []string - wantWebhookOpts WebhookOptions + wantWebhookOpts WebhookAndAdmissionPolicyOptions wantErred bool wantErrMsgSubStr string }{ @@ -720,7 +721,7 @@ func TestWebhookOptions(t *testing.T) { name: "all default", flagSetName: "allDefault", args: []string{}, - wantWebhookOpts: WebhookOptions{ + wantWebhookOpts: WebhookAndAdmissionPolicyOptions{ EnableWebhooks: true, ClientConnectionType: "url", ServiceName: "fleetwebhook", @@ -730,6 +731,8 @@ func TestWebhookOptions(t *testing.T) { EnableWorkload: false, EnablePDBs: true, UseCertManager: false, + EnableAdmissionPolicyManager: false, + AdmissionPolicyManagerConfig: "", }, }, { @@ -744,8 +747,10 @@ func TestWebhookOptions(t *testing.T) { "--deny-modify-member-cluster-labels=true", "--enable-workload=true", "--use-cert-manager=true", + "--enable-admission-policy-manager=true", + "--admission-policy-manager-config=/etc/fleet/policy-config.json", }, - wantWebhookOpts: WebhookOptions{ + wantWebhookOpts: WebhookAndAdmissionPolicyOptions{ EnableWebhooks: false, ClientConnectionType: "service", ServiceName: "customwebhook", @@ -755,13 +760,15 @@ func TestWebhookOptions(t *testing.T) { EnableWorkload: true, EnablePDBs: true, UseCertManager: true, + EnableAdmissionPolicyManager: true, + AdmissionPolicyManagerConfig: "/etc/fleet/policy-config.json", }, }, { name: "webhook client connection type URL (case-insensitive)", flagSetName: "webhookClientConnTypeURL", args: []string{"--webhook-client-connection-type=URL"}, - wantWebhookOpts: WebhookOptions{ + wantWebhookOpts: WebhookAndAdmissionPolicyOptions{ EnableWebhooks: true, ClientConnectionType: "url", ServiceName: "fleetwebhook", @@ -771,13 +778,15 @@ func TestWebhookOptions(t *testing.T) { EnableWorkload: false, EnablePDBs: true, UseCertManager: false, + EnableAdmissionPolicyManager: false, + AdmissionPolicyManagerConfig: "", }, }, { name: "webhook client connection type service (case-insensitive)", flagSetName: "webhookClientConnTypeService", args: []string{"--webhook-client-connection-type=Service"}, - wantWebhookOpts: WebhookOptions{ + wantWebhookOpts: WebhookAndAdmissionPolicyOptions{ EnableWebhooks: true, ClientConnectionType: "service", ServiceName: "fleetwebhook", @@ -787,6 +796,8 @@ func TestWebhookOptions(t *testing.T) { EnableWorkload: false, EnablePDBs: true, UseCertManager: false, + EnableAdmissionPolicyManager: false, + AdmissionPolicyManagerConfig: "", }, }, { @@ -801,8 +812,8 @@ func TestWebhookOptions(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { flags := flag.NewFlagSet(tc.flagSetName, flag.ContinueOnError) - webhookOpts := WebhookOptions{} - webhookOpts.AddFlags(flags) + webhookAndAdmissionPolicyOpts := WebhookAndAdmissionPolicyOptions{} + webhookAndAdmissionPolicyOpts.AddFlags(flags) err := flags.Parse(tc.args) if tc.wantErred { @@ -820,7 +831,7 @@ func TestWebhookOptions(t *testing.T) { t.Fatalf("flag Parse() = %v, want nil", err) } - if diff := cmp.Diff(webhookOpts, tc.wantWebhookOpts); diff != "" { + if diff := cmp.Diff(webhookAndAdmissionPolicyOpts, tc.wantWebhookOpts); diff != "" { t.Errorf("webhook options diff (-got, +want):\n%s", diff) } }) diff --git a/cmd/hubagent/options/validation.go b/cmd/hubagent/options/validation.go index 5770be49d..49f3d90f9 100644 --- a/cmd/hubagent/options/validation.go +++ b/cmd/hubagent/options/validation.go @@ -17,7 +17,12 @@ limitations under the License. package options import ( + "os" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/kubefleet-dev/kubefleet/pkg/admissionpolicymanager" ) // Validate checks Options and return a slice of found errs. @@ -45,12 +50,12 @@ func (o *Options) Validate() field.ErrorList { // but here the logic enforces that a service name must be provided. The way we handle // URLs is also problematic as the code will always format a service-targeted URL using // the input. We keep this logic for now for compatibility reasons. - if o.WebhookOpts.EnableWebhooks && o.WebhookOpts.ServiceName == "" { - errs = append(errs, field.Invalid(newPath.Child("WebhookServiceName"), o.WebhookOpts.ServiceName, "A webhook service name is required when webhooks are enabled")) + if o.WebhookAndAdmissionPolicyOpts.EnableWebhooks && o.WebhookAndAdmissionPolicyOpts.ServiceName == "" { + errs = append(errs, field.Invalid(newPath.Child("WebhookServiceName"), o.WebhookAndAdmissionPolicyOpts.ServiceName, "A webhook service name is required when webhooks are enabled")) } - if o.WebhookOpts.UseCertManager && !o.WebhookOpts.EnableWorkload { - errs = append(errs, field.Invalid(newPath.Child("UseCertManager"), o.WebhookOpts.UseCertManager, "If cert manager is used for securing webhook connections, the EnableWorkload option must be set to true, so that cert manager pods can run in the hub cluster.")) + if o.WebhookAndAdmissionPolicyOpts.UseCertManager && !o.WebhookAndAdmissionPolicyOpts.EnableWorkload { + errs = append(errs, field.Invalid(newPath.Child("UseCertManager"), o.WebhookAndAdmissionPolicyOpts.UseCertManager, "If cert manager is used for securing webhook connections, the EnableWorkload option must be set to true, so that cert manager pods can run in the hub cluster.")) } if o.PlacementMgmtOpts.AllowedPropagatingAPIs != "" && o.PlacementMgmtOpts.SkippedPropagatingAPIs != "" { @@ -66,5 +71,33 @@ func (o *Options) Validate() field.ErrorList { errs = append(errs, field.Invalid(newPath.Child("PlacementControllerWorkQueueRateLimiterOpts").Child("RateLimiterQPS"), o.PlacementMgmtOpts.PlacementControllerWorkQueueRateLimiterOpts.RateLimiterQPS, "the QPS for the placement controller set rate limiter must be less than its bucket size")) } + // Validate admission policy manager setup (if enabled). + if err := o.validateAdmissionPolicyManagerConfig(newPath); err != nil { + errs = append(errs, err) + } + return errs } + +func (o *Options) validateAdmissionPolicyManagerConfig(newPath *field.Path) *field.Error { + if o.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager { + managerConfigPath := o.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig + if len(managerConfigPath) != 0 { + // Read the file from the path. + data, err := os.ReadFile(managerConfigPath) + if err != nil { + return field.Invalid(newPath.Child("AdmissionPolicyManagerConfig"), managerConfigPath, "failed to read the admission policy manager config file: "+err.Error()) + } + + policyGeneratorConfigs := &admissionpolicymanager.PolicyGeneratorConfigs{} + if err := yaml.Unmarshal(data, policyGeneratorConfigs); err != nil { + return field.Invalid(newPath.Child("AdmissionPolicyManagerConfig"), managerConfigPath, "failed to unmarshal the admission policy manager config file: "+err.Error()) + } + + if err := policyGeneratorConfigs.Validate(); err != nil { + return field.Invalid(newPath.Child("AdmissionPolicyManagerConfig"), managerConfigPath, "invalid admission policy manager config: "+err.Error()) + } + } + } + return nil +} diff --git a/cmd/hubagent/options/validation_test.go b/cmd/hubagent/options/validation_test.go index b67d84571..24bea5815 100644 --- a/cmd/hubagent/options/validation_test.go +++ b/cmd/hubagent/options/validation_test.go @@ -18,12 +18,17 @@ package options import ( "flag" + "os" + "path/filepath" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/yaml" + + "github.com/kubefleet-dev/kubefleet/pkg/admissionpolicymanager" ) const ( @@ -44,7 +49,7 @@ func newTestOptions(modifyOptions ModifyOptions) Options { LeaderElectionQPS: 250.0, LeaderElectionBurst: 1000, }, - WebhookOpts: WebhookOptions{ + WebhookAndAdmissionPolicyOpts: WebhookAndAdmissionPolicyOptions{ ClientConnectionType: "url", ServiceName: testWebhookServiceName, }, @@ -65,7 +70,7 @@ func newTestOptions(modifyOptions ModifyOptions) Options { return option } -func TestValidateControllerManagerConfiguration(t *testing.T) { +func TestValidation(t *testing.T) { newPath := field.NewPath("Options") testCases := map[string]struct { opt Options @@ -91,26 +96,26 @@ func TestValidateControllerManagerConfiguration(t *testing.T) { }, "WebhookServiceName is empty": { opt: newTestOptions(func(option *Options) { - option.WebhookOpts.EnableWebhooks = true - option.WebhookOpts.ServiceName = "" + option.WebhookAndAdmissionPolicyOpts.EnableWebhooks = true + option.WebhookAndAdmissionPolicyOpts.ServiceName = "" }), want: field.ErrorList{field.Invalid(newPath.Child("WebhookServiceName"), "", "A webhook service name is required when webhooks are enabled")}, }, "UseCertManager without EnableWorkload": { opt: newTestOptions(func(option *Options) { - option.WebhookOpts.EnableWebhooks = true - option.WebhookOpts.ServiceName = testWebhookServiceName - option.WebhookOpts.UseCertManager = true - option.WebhookOpts.EnableWorkload = false + option.WebhookAndAdmissionPolicyOpts.EnableWebhooks = true + option.WebhookAndAdmissionPolicyOpts.ServiceName = testWebhookServiceName + option.WebhookAndAdmissionPolicyOpts.UseCertManager = true + option.WebhookAndAdmissionPolicyOpts.EnableWorkload = false }), want: field.ErrorList{field.Invalid(newPath.Child("UseCertManager"), true, "If cert manager is used for securing webhook connections, the EnableWorkload option must be set to true, so that cert manager pods can run in the hub cluster.")}, }, "UseCertManager with EnableWebhook and EnableWorkload": { opt: newTestOptions(func(option *Options) { - option.WebhookOpts.EnableWebhooks = true - option.WebhookOpts.ServiceName = testWebhookServiceName - option.WebhookOpts.UseCertManager = true - option.WebhookOpts.EnableWorkload = true + option.WebhookAndAdmissionPolicyOpts.EnableWebhooks = true + option.WebhookAndAdmissionPolicyOpts.ServiceName = testWebhookServiceName + option.WebhookAndAdmissionPolicyOpts.UseCertManager = true + option.WebhookAndAdmissionPolicyOpts.EnableWorkload = true }), want: field.ErrorList{}, }, @@ -146,6 +151,103 @@ func TestValidateControllerManagerConfiguration(t *testing.T) { } } +func TestValidateAdmissionPolicyManagerConfig(t *testing.T) { + newPath := field.NewPath("Options") + tmpDir := t.TempDir() + + // Non-existent file: capture the OS error to mirror the exact detail string. + nonExistentPath := filepath.Join(tmpDir, "nonexistent.yaml") + _, readErr := os.ReadFile(nonExistentPath) + + // Invalid YAML file. + invalidYAMLPath := filepath.Join(tmpDir, "invalid.yaml") + if err := os.WriteFile(invalidYAMLPath, []byte("[unclosed"), 0600); err != nil { + t.Fatalf("TestValidateAdmissionPolicyManagerConfig: failed to write invalid YAML file: %v", err) + } + var dummy admissionpolicymanager.PolicyGeneratorConfigs + yamlErr := yaml.Unmarshal([]byte("[unclosed"), &dummy) + + // Config file that passes YAML parsing but fails Validate() (empty ReservedNamespacePrefixes). + invalidConfig := admissionpolicymanager.PolicyGeneratorConfigs{ + PodsAndReplicaSetsVAPGeneratorConfig: &admissionpolicymanager.PodsAndReplicaSetsValidatingAdmissionPolicyGenerator{ + ReservedNamespacePrefixes: []string{}, + }, + } + invalidConfigData, _ := yaml.Marshal(invalidConfig) + invalidConfigPath := filepath.Join(tmpDir, "invalid-config.yaml") + if err := os.WriteFile(invalidConfigPath, invalidConfigData, 0600); err != nil { + t.Fatalf("TestValidateAdmissionPolicyManagerConfig: failed to write invalid config file: %v", err) + } + configValidateErr := invalidConfig.Validate() + + // Valid config file. + validConfig := admissionpolicymanager.PolicyGeneratorConfigs{ + PodsAndReplicaSetsVAPGeneratorConfig: &admissionpolicymanager.PodsAndReplicaSetsValidatingAdmissionPolicyGenerator{ + ReservedNamespacePrefixes: []string{"fleet-", "kube-"}, + }, + } + validConfigData, _ := yaml.Marshal(validConfig) + validConfigPath := filepath.Join(tmpDir, "valid-config.yaml") + if err := os.WriteFile(validConfigPath, validConfigData, 0600); err != nil { + t.Fatalf("TestValidateAdmissionPolicyManagerConfig: failed to write valid config file: %v", err) + } + + testCases := map[string]struct { + opt Options + want field.ErrorList + }{ + "admission policy manager enabled, no config path specified": { + opt: newTestOptions(func(option *Options) { + option.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager = true + }), + want: field.ErrorList{}, + }, + "admission policy manager enabled, config file does not exist": { + opt: newTestOptions(func(option *Options) { + option.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager = true + option.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig = nonExistentPath + }), + want: field.ErrorList{ + field.Invalid(newPath.Child("AdmissionPolicyManagerConfig"), nonExistentPath, "failed to read the admission policy manager config file: "+readErr.Error()), + }, + }, + "admission policy manager enabled, config file contains invalid YAML": { + opt: newTestOptions(func(option *Options) { + option.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager = true + option.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig = invalidYAMLPath + }), + want: field.ErrorList{ + field.Invalid(newPath.Child("AdmissionPolicyManagerConfig"), invalidYAMLPath, "failed to unmarshal the admission policy manager config file: "+yamlErr.Error()), + }, + }, + "admission policy manager enabled, config file fails validation": { + opt: newTestOptions(func(option *Options) { + option.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager = true + option.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig = invalidConfigPath + }), + want: field.ErrorList{ + field.Invalid(newPath.Child("AdmissionPolicyManagerConfig"), invalidConfigPath, "invalid admission policy manager config: "+configValidateErr.Error()), + }, + }, + "admission policy manager enabled, valid config file": { + opt: newTestOptions(func(option *Options) { + option.WebhookAndAdmissionPolicyOpts.EnableAdmissionPolicyManager = true + option.WebhookAndAdmissionPolicyOpts.AdmissionPolicyManagerConfig = validConfigPath + }), + want: field.ErrorList{}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.opt.Validate() + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Validate() errs mismatch (-want, +got):\n%s", diff) + } + }) + } +} + func TestAddFlags(t *testing.T) { g := gomega.NewWithT(t) opts := NewOptions() @@ -153,5 +255,5 @@ func TestAddFlags(t *testing.T) { flags := flag.NewFlagSet("deny-modify-member-cluster-labels", flag.ExitOnError) opts.AddFlags(flags) - g.Expect(opts.WebhookOpts.GuardRailDenyModifyMemberClusterLabels).To(gomega.BeFalse(), "deny-modify-member-cluster-labels should be false by default") + g.Expect(opts.WebhookAndAdmissionPolicyOpts.GuardRailDenyModifyMemberClusterLabels).To(gomega.BeFalse(), "deny-modify-member-cluster-labels should be false by default") } diff --git a/cmd/hubagent/options/webhooks.go b/cmd/hubagent/options/webhooks.go index 5882b5210..ef7d61494 100644 --- a/cmd/hubagent/options/webhooks.go +++ b/cmd/hubagent/options/webhooks.go @@ -21,9 +21,9 @@ import ( "fmt" ) -// WebhookOptions is a set of options the KubeFleet hub agent exposes for -// controlling webhook behavior. -type WebhookOptions struct { +// WebhookAndAdmissionPolicyOptions is a set of options the KubeFleet hub agent exposes for +// controlling webhook and admission policy behavior. +type WebhookAndAdmissionPolicyOptions struct { // Enable the KubeFleet webhooks or not. EnableWebhooks bool @@ -65,10 +65,29 @@ type WebhookOptions struct { // If set to false, the system will use self-signed certificates. // This option only applies if webhooks are enabled. UseCertManager bool + + // Enable the KubeFleet admission policy manager or not. + // + // KubeFleet admission policy manager manages admission policies that help enforce and validate + // certain behaviors, which serves as an in-process, more performant and more available alternative + // to some of the applicable KubeFleet validating webhooks. The manager installs and uninstalls + // admission policies when the hub agent starts. + // + // TO-DO (chenyu1): for upstream deployments, allow users to use the hub agent Helm chart to + // manage the lifecycle of the admission policies. The manager is reserved for environments + // where Helm based setup is not possible or not desired. + EnableAdmissionPolicyManager bool + + // A file path to the configuration file for the KubeFleet admission policy manager. The file + // is a YAML file that specifies configuration for each policy generator available + // in the admission policy manager. See the KubeFleet source code for more information. + // If not specified, the default configuration will be used, which enables all available + // policy generators. This option only applies if the admission policy manager is enabled. + AdmissionPolicyManagerConfig string } -// AddFlags adds flags for WebhookOptions to the specified FlagSet. -func (o *WebhookOptions) AddFlags(flags *flag.FlagSet) { +// AddFlags adds flags for WebhookAndAdmissionPolicyOptions to the specified FlagSet. +func (o *WebhookAndAdmissionPolicyOptions) AddFlags(flags *flag.FlagSet) { flags.BoolVar( &o.EnableWebhooks, "enable-webhook", @@ -131,6 +150,20 @@ func (o *WebhookOptions) AddFlags(flags *flag.FlagSet) { false, "Use the cert-manager project for managing KubeFleet webhook server certificates or not. If set to false, the system will use self-signed certificates. If set to true, the EnableWorkload option must be set to true as well. This option only applies if webhooks are enabled.", ) + + flags.BoolVar( + &o.EnableAdmissionPolicyManager, + "enable-admission-policy-manager", + false, + "Enable the KubeFleet admission policy manager or not. KubeFleet admission policy manager manages admission policies that help enforce and validate certain behaviors, which serves as an in-process, more performant and more available alternative to some of the applicable KubeFleet validating webhooks.", + ) + + flags.StringVar( + &o.AdmissionPolicyManagerConfig, + "admission-policy-manager-config", + "", + "A file path to the configuration file for the KubeFleet admission policy manager. The file is a JSON or YAML file that specifies configuration for each policy generator available in the admission policy manager. See the KubeFleet source code for more information. If not specified, the default configuration will be used, which enables all available policy generators. This option only applies if the admission policy manager is enabled.", + ) } type WebhookClientConnTypeValueWithValidation string diff --git a/cmd/hubagent/workload/setup.go b/cmd/hubagent/workload/setup.go index b603978cc..ec895061f 100644 --- a/cmd/hubagent/workload/setup.go +++ b/cmd/hubagent/workload/setup.go @@ -162,7 +162,7 @@ func SetupControllers(ctx context.Context, wg *sync.WaitGroup, mgr ctrl.Manager, InformerManager: dynamicInformerManager, ResourceConfig: resourceConfig, SkippedNamespaces: skippedNamespaces, - EnableWorkload: opts.WebhookOpts.EnableWorkload, + EnableWorkload: opts.WebhookAndAdmissionPolicyOpts.EnableWorkload, } resourceSnapshotResolver := controller.NewResourceSnapshotResolver(mgr.GetClient(), mgr.GetScheme()) resourceSnapshotResolver.Config = controller.NewResourceSnapshotConfig(opts.PlacementMgmtOpts.ResourceSnapshotCreationMinimumInterval, opts.PlacementMgmtOpts.ResourceChangesCollectionDuration) @@ -532,7 +532,7 @@ func SetupControllers(ctx context.Context, wg *sync.WaitGroup, mgr ctrl.Manager, SkippedNamespaces: skippedNamespaces, ConcurrentPlacementWorker: int(math.Ceil(float64(opts.PlacementMgmtOpts.MaxConcurrentClusterPlacement) / 10)), ConcurrentResourceChangeWorker: opts.PlacementMgmtOpts.ConcurrentResourceChangeSyncs, - EnableWorkload: opts.WebhookOpts.EnableWorkload, + EnableWorkload: opts.WebhookAndAdmissionPolicyOpts.EnableWorkload, } if err := mgr.Add(resourceChangeDetector); err != nil { diff --git a/pkg/admissionpolicymanager/configs.go b/pkg/admissionpolicymanager/configs.go index cd9106e22..818a40d84 100644 --- a/pkg/admissionpolicymanager/configs.go +++ b/pkg/admissionpolicymanager/configs.go @@ -17,7 +17,12 @@ limitations under the License. package admissionpolicymanager import ( + "reflect" + + "k8s.io/apimachinery/pkg/util/sets" + "github.com/kubefleet-dev/kubefleet/pkg/utils" + "github.com/kubefleet-dev/kubefleet/pkg/utils/errors" ) // PolicyGeneratorConfigs holds the configurations for all available admission policy @@ -39,3 +44,48 @@ var DefaultPolicyGeneratorConfigs = &PolicyGeneratorConfigs{ ReservedNamespacePrefixes: []string{utils.FleetNSNamePrefix, utils.KubeNSNamePrefix}, }, } + +// Validate validates each generator configuration in the given PolicyGeneratorConfigs. +func (config *PolicyGeneratorConfigs) Validate() error { + if config == nil { + return nil + } + + v := reflect.ValueOf(config).Elem() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.IsNil() { + continue + } + gen, ok := field.Interface().(ValidatingAdmissionPolicyGenerator) + if !ok { + continue + } + if err := gen.Validate(); err != nil { + return errors.Wraps(err, "one of the admission policy generators is invalid", "generator", gen.Name()) + } + } + return nil +} + +// EnabledGenerators returns the set of names of generators that are enabled in the configuration. +func (config *PolicyGeneratorConfigs) EnabledGenerators() sets.Set[string] { + enabled := sets.New[string]() + if config == nil { + return enabled + } + + v := reflect.ValueOf(config).Elem() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.IsNil() { + continue + } + gen, ok := field.Interface().(ValidatingAdmissionPolicyGenerator) + if !ok { + continue + } + enabled.Insert(gen.Name()) + } + return enabled +} diff --git a/pkg/admissionpolicymanager/manager.go b/pkg/admissionpolicymanager/manager.go index 8d4c7b936..9d8b0f6c3 100644 --- a/pkg/admissionpolicymanager/manager.go +++ b/pkg/admissionpolicymanager/manager.go @@ -97,13 +97,13 @@ type PolicyManager struct { enabledPolicyGenerators map[string]ValidatingAdmissionPolicyGenerator } -func New(client client.Client, policyGeneratorConfigs *PolicyGeneratorConfigs, enabledPolicyNames []string) (*PolicyManager, error) { +func New(client client.Client, policyGeneratorConfigs *PolicyGeneratorConfigs) (*PolicyManager, error) { if policyGeneratorConfigs == nil { klog.V(2).Info("No admission policy generator configuration provided, falling back to the default configuration") policyGeneratorConfigs = DefaultPolicyGeneratorConfigs } - // Prepare a set of generators based on the list of enabled policies. - enabledPolicyGenerators, err := preparePolicyGenerators(policyGeneratorConfigs, enabledPolicyNames) + // Prepare a set of generators based on the given configuration. + enabledPolicyGenerators, err := preparePolicyGenerators(policyGeneratorConfigs) if err != nil { return nil, errors.Wraps(err, "failed to create policy manager") } @@ -114,11 +114,7 @@ func New(client client.Client, policyGeneratorConfigs *PolicyGeneratorConfigs, e }, nil } -func preparePolicyGenerators( - policyGeneratorConfigs *PolicyGeneratorConfigs, - enabledPolicyNames []string, -) (map[string]ValidatingAdmissionPolicyGenerator, error) { - enabledPolicyNameSet := sets.New(enabledPolicyNames...) +func preparePolicyGenerators(policyGeneratorConfigs *PolicyGeneratorConfigs) (map[string]ValidatingAdmissionPolicyGenerator, error) { enabledPolicyGenerators := make(map[string]ValidatingAdmissionPolicyGenerator) v := reflect.ValueOf(policyGeneratorConfigs).Elem() @@ -131,18 +127,11 @@ func preparePolicyGenerators( if !ok { continue } - if enabledPolicyNameSet.Has(gen.Name()) { + if gen != nil { enabledPolicyGenerators[gen.Name()] = gen } } - if len(enabledPolicyNameSet) != len(enabledPolicyGenerators) { - configuredPolicyNames := make([]string, 0, len(enabledPolicyGenerators)) - for name := range enabledPolicyGenerators { - configuredPolicyNames = append(configuredPolicyNames, name) - } - return nil, errors.NewUserError(nil, "some enabled policy generators are not configured properly", "enabledPolicies", enabledPolicyNames, "configuredPolicies", configuredPolicyNames) - } return enabledPolicyGenerators, nil } diff --git a/pkg/admissionpolicymanager/manager_integration_test.go b/pkg/admissionpolicymanager/manager_integration_test.go index 6c9d076ed..37b1a240b 100644 --- a/pkg/admissionpolicymanager/manager_integration_test.go +++ b/pkg/admissionpolicymanager/manager_integration_test.go @@ -166,7 +166,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, Validations: []admissionregistrationv1.Validation{ { - Expression: `!(request.namespace.startsWith("fleet-") || request.namespace.startsWith("kube-")) || (request.userInfo.username == "system:kube-scheduler" || request.userInfo.username == "system:kube-controller-manager" || "system:nodes" in request.userInfo.groups || "system:masters" in request.userInfo.groups)`, + Expression: `!(request.namespace.startsWith("fleet-") || request.namespace.startsWith("kube-")) || (request.userInfo.username == "system:kube-scheduler" || request.userInfo.username == "system:kube-controller-manager" || "system:nodes" in request.userInfo.groups || "system:masters" in request.userInfo.groups || "kubeadm:cluster-admins" in request.userInfo.groups || "system:serviceaccounts" in request.userInfo.groups)`, Message: "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed", Reason: ptr.To(metav1.StatusReasonForbidden), }, diff --git a/pkg/admissionpolicymanager/suite_test.go b/pkg/admissionpolicymanager/suite_test.go index 45df92091..4371ceb30 100644 --- a/pkg/admissionpolicymanager/suite_test.go +++ b/pkg/admissionpolicymanager/suite_test.go @@ -94,11 +94,7 @@ var _ = BeforeSuite(func() { Expect(hubUncachedClient).ToNot(BeNil()) By("Setting up the policy manager") - enabledGeneratorNames := []string{} - for name := range allGenerators { - enabledGeneratorNames = append(enabledGeneratorNames, name) - } - policyManager, err := New(hubUncachedClient, DefaultPolicyGeneratorConfigs, enabledGeneratorNames) + policyManager, err := New(hubUncachedClient, DefaultPolicyGeneratorConfigs) Expect(err).ToNot(HaveOccurred()) Expect(policyManager).ToNot(BeNil()) Expect(policyManager.Start(ctx)).To(Succeed()) diff --git a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go index 16aa46d4b..33f1ecc17 100644 --- a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go +++ b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go @@ -44,8 +44,10 @@ const ( kubeSchedulerUserName = "system:kube-scheduler" kubeControllerManagerUserName = "system:kube-controller-manager" - kubeNodeUserGroup = "system:nodes" - adminUserGroup = "system:masters" + kubeNodeUserGroup = "system:nodes" + adminUserGroup = "system:masters" + kubeadmAdminUserGroup = "kubeadm:cluster-admins" + svcAccountUserGroup = "system:serviceaccounts" ) // Verify that ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator implements @@ -119,8 +121,16 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Poli celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, kubeSchedulerUserName)) celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, kubeControllerManagerUserName)) celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, kubeNodeUserGroup)) - // Exempt requests from admin users from this admission policy. + // Exempt requests from cluster admin users from this admission policy. celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, adminUserGroup)) + // Exempt kubeadm cluster admins from this policy as well, so that bootstrapping a hub cluster with + // kubeadm credentials can proceed without being blocked. + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, kubeadmAdminUserGroup)) + // Exempt service accounts from this admission policy. Note that VAP check happens after authentication and + // authorization have been performed. This is added to keep things consistent with the original webhook behavior, + // and also for the reason that some controller manager components (e.g., the service account controller) + // need to create service accounts as part of their normal operations. + celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, svcAccountUserGroup)) celExprAcc := strings.Join(celExprAccSegs, " || ") diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 9f2d921b0..f25fd9d88 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -244,20 +244,20 @@ func NewWebhookConfig( // String-to-enum conversions (e.g., WebhookClientConnectionType) are performed without // additional validation, as validation happens at the Options level. func NewWebhookConfigFromOptions(mgr manager.Manager, opts *options.Options, webhookPort int32) (*Config, error) { - webhookClientConnectionType := options.WebhookClientConnectionType(opts.WebhookOpts.ClientConnectionType) - whiteListedUsers := strings.Split(opts.WebhookOpts.GuardRailWhitelistedUsers, ",") + webhookClientConnectionType := options.WebhookClientConnectionType(opts.WebhookAndAdmissionPolicyOpts.ClientConnectionType) + whiteListedUsers := strings.Split(opts.WebhookAndAdmissionPolicyOpts.GuardRailWhitelistedUsers, ",") return NewWebhookConfig( mgr, - opts.WebhookOpts.ServiceName, + opts.WebhookAndAdmissionPolicyOpts.ServiceName, webhookPort, &webhookClientConnectionType, FleetWebhookCertDir, - opts.WebhookOpts.EnableGuardRail, - opts.WebhookOpts.GuardRailDenyModifyMemberClusterLabels, - opts.WebhookOpts.EnableWorkload, - opts.WebhookOpts.EnablePDBs, - opts.WebhookOpts.UseCertManager, + opts.WebhookAndAdmissionPolicyOpts.EnableGuardRail, + opts.WebhookAndAdmissionPolicyOpts.GuardRailDenyModifyMemberClusterLabels, + opts.WebhookAndAdmissionPolicyOpts.EnableWorkload, + opts.WebhookAndAdmissionPolicyOpts.EnablePDBs, + opts.WebhookAndAdmissionPolicyOpts.UseCertManager, FleetWebhookCertName, whiteListedUsers, opts.ClusterMgmtOpts.NetworkingAgentsEnabled) diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go index 6f00c7ae3..d4c1d6d55 100644 --- a/pkg/webhook/webhook_test.go +++ b/pkg/webhook/webhook_test.go @@ -254,7 +254,7 @@ func TestNewWebhookConfigFromOptions(t *testing.T) { }{ "valid options with cert-manager": { opts: &options.Options{ - WebhookOpts: options.WebhookOptions{ + WebhookAndAdmissionPolicyOpts: options.WebhookAndAdmissionPolicyOptions{ ServiceName: "test-webhook", ClientConnectionType: "service", EnableGuardRail: true, @@ -285,7 +285,7 @@ func TestNewWebhookConfigFromOptions(t *testing.T) { }, "valid options without cert-manager": { opts: &options.Options{ - WebhookOpts: options.WebhookOptions{ + WebhookAndAdmissionPolicyOpts: options.WebhookAndAdmissionPolicyOptions{ ServiceName: "test-webhook", ClientConnectionType: "url", EnableGuardRail: false, diff --git a/test/e2e/admission_policies_test.go b/test/e2e/admission_policies_test.go new file mode 100644 index 000000000..a97da3170 --- /dev/null +++ b/test/e2e/admission_policies_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2025 The KubeFleet Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + + "github.com/kubefleet-dev/kubefleet/pkg/admissionpolicymanager" +) + +var _ = Describe("deny service account writes and token requests in restricted namespaces via VAP", Ordered, func() { + svcAccountName := fmt.Sprintf(svcAccountNameTemplate, GinkgoParallelProcess()) + svcAccountToAddName := "added-sa" + kubeSystemNamespaceName := "kube-system" + + var svcAccount *corev1.ServiceAccount + BeforeAll(func() { + if !EnabledVAPGenerators.Has(admissionpolicymanager.SvcAccountsAndTokenRequestsVAPGeneratorName) { + Skip("VAP required for this test is not enabled; skip the test") + } + + // Create a service account in the kube-system namespace. + svcAccount = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountName, + Namespace: kubeSystemNamespaceName, + }, + } + Expect(hubClient.Create(ctx, svcAccount)).Should(Succeed(), "Failed to create service account") + }) + + AfterAll(func() { + // Ensure the removal of the created service account. + Eventually(func() error { + svcAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountName, + Namespace: kubeSystemNamespaceName, + }, + } + if err := hubClient.Delete(ctx, &svcAccount); err != nil { + if k8sErrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to delete service account: %w", err) + } + + if err := hubClient.Get(ctx, types.NamespacedName{Name: svcAccountName, Namespace: kubeSystemNamespaceName}, &corev1.ServiceAccount{}); err != nil { + if k8sErrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to retrieve service account: %w", err) + } + return fmt.Errorf("service account still exists") + }, eventuallyDuration, eventuallyInterval).Should(Succeed(), "Failed to delete service account") + }) + + It("should deny creation of service accounts in kube-system namespace for non-whitelisted users", func() { + newSvcAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: svcAccountToAddName, + Namespace: kubeSystemNamespaceName, + }, + } + wantErrMsg := "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed" + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.Create(ctx, newSvcAccount), wantErrMsg)).Should(Succeed(), "Failed to deny creation of service account in kube-system namespace") + }) + + It("should deny access to service account token subresource in restricted namespace for non-whitelisted users", func() { + tokenRequest := &authenticationv1.TokenRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: kubeSystemNamespaceName, + Name: svcAccountName, + }, + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"experimental"}, + ExpirationSeconds: ptr.To(int64(3600)), + }, + } + wantErrMsg := "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed" + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.SubResource("token").Create(ctx, svcAccount, tokenRequest), wantErrMsg)).Should(Succeed(), "Failed to deny access to service account token subresource in restricted namespace") + }) +}) + +var _ = Describe("deny pod and replica set creation in non-reserved namespaces via VAP", Ordered, func() { + podName := "dummy-pod" + replicaSetName := "dummy-replica-set" + + BeforeAll(func() { + if !EnabledVAPGenerators.Has(admissionpolicymanager.PodsAndReplicaSetsVAPGeneratorName) { + Skip("VAP required for this test is not enabled; skip the test") + } + }) + + AfterAll(func() { + // Ensure the removal of the pod in case the test fails and the object was inadvertently created. + Eventually(func() error { + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: "default", + }, + } + if err := hubClient.Delete(ctx, &pod); err != nil { + if k8sErrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to delete pod: %w", err) + } + + if err := hubClient.Get(ctx, types.NamespacedName{Name: podName, Namespace: "default"}, &corev1.Pod{}); err != nil { + if k8sErrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to retrieve pod: %w", err) + } + return fmt.Errorf("pod still exists") + }, eventuallyDuration, eventuallyInterval).Should(Succeed(), "Failed to delete pod") + + // Ensure the removal of the replica set in case the test fails and the object was inadvertently created. + Eventually(func() error { + replicaSet := appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: replicaSetName, + Namespace: "default", + }, + } + if err := hubClient.Delete(ctx, &replicaSet); err != nil { + if k8sErrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to delete replica set: %w", err) + } + + if err := hubClient.Get(ctx, types.NamespacedName{Name: replicaSetName, Namespace: "default"}, &appsv1.ReplicaSet{}); err != nil { + if k8sErrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to retrieve replica set: %w", err) + } + return fmt.Errorf("replica set still exists") + }, eventuallyDuration, eventuallyInterval).Should(Succeed(), "Failed to delete replica set") + }) + + It("should deny creation of pods in the default namespace for non-whitelisted users", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx", + }, + }, + }, + } + wantErrMsg := "creating pods and replicas is disallowed in the fleet hub cluster" + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.Create(ctx, pod), wantErrMsg)).Should(Succeed(), "Failed to deny creation of pod in default namespace") + }) + + It("should deny creation of replica sets in the default namespace for non-whitelisted users", func() { + replicaSet := &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: replicaSetName, + Namespace: "default", + }, + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "nginx"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "nginx"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx", + }, + }, + }, + }, + }, + } + wantErrMsg := "creating pods and replicas is disallowed in the fleet hub cluster" + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.Create(ctx, replicaSet), wantErrMsg)).Should(Succeed(), "Failed to deny creation of replica set in default namespace") + }) +}) diff --git a/test/e2e/admission_policy_manager_cfg.yaml b/test/e2e/admission_policy_manager_cfg.yaml new file mode 100644 index 000000000..234441e6a --- /dev/null +++ b/test/e2e/admission_policy_manager_cfg.yaml @@ -0,0 +1,4 @@ +denyServiceAccountsAndTokenRequestsInReservedNamespaces: + ReservedNamespacePrefixes: + - fleet- + - kube- diff --git a/test/e2e/fleet_guard_rail_test.go b/test/e2e/fleet_guard_rail_test.go index 043304783..9b391b2e7 100644 --- a/test/e2e/fleet_guard_rail_test.go +++ b/test/e2e/fleet_guard_rail_test.go @@ -39,6 +39,7 @@ import ( clusterv1beta1 "github.com/kubefleet-dev/kubefleet/apis/cluster/v1beta1" placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" + "github.com/kubefleet-dev/kubefleet/pkg/admissionpolicymanager" "github.com/kubefleet-dev/kubefleet/pkg/utils" "github.com/kubefleet-dev/kubefleet/pkg/webhook/validation" @@ -1316,6 +1317,10 @@ var _ = Describe("fleet guard rail webhook tests for service accounts in restric var svcAccount *corev1.ServiceAccount BeforeAll(func() { + if EnabledVAPGenerators.Has(admissionpolicymanager.SvcAccountsAndTokenRequestsVAPGeneratorName) { + Skip("A VAP that serves the same purpose is enabled; skip this test as the request will be rejected by the VAP instead") + } + // Create a service account in the kube-system namespace. svcAccount = &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e/setup.sh b/test/e2e/setup.sh index 34214a727..0f297084a 100755 --- a/test/e2e/setup.sh +++ b/test/e2e/setup.sh @@ -150,6 +150,9 @@ helm install hub-agent ../../charts/hub-agent/ \ --set clusterUnhealthyThreshold="3m0s" \ --set logFileMaxSize=100000 \ --set MaxConcurrentClusterPlacement=200 \ + --set-file additionalConfigData.admissionPolicyManagerCfg=admission_policy_manager_cfg.yaml \ + --set admissionPolicyManagerConfigName=admissionPolicyManagerCfg \ + --set enableAdmissionPolicyManager=true \ --set resourceSnapshotCreationMinimumInterval=$RESOURCE_SNAPSHOT_CREATION_MINIMUM_INTERVAL \ --set resourceChangesCollectionDuration=$RESOURCE_CHANGES_COLLECTION_DURATION \ --wait \ diff --git a/test/e2e/setup_test.go b/test/e2e/setup_test.go index c5754e63b..5786862e5 100644 --- a/test/e2e/setup_test.go +++ b/test/e2e/setup_test.go @@ -35,12 +35,14 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" k8sscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/yaml" fleetnetworkingv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" @@ -48,6 +50,7 @@ import ( placementv1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1" placementv1alpha1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1alpha1" placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" + "github.com/kubefleet-dev/kubefleet/pkg/admissionpolicymanager" "github.com/kubefleet-dev/kubefleet/pkg/propertyprovider/azure/trackers" "github.com/kubefleet-dev/kubefleet/pkg/utils" testv1alpha1 "github.com/kubefleet-dev/kubefleet/test/apis/v1alpha1" @@ -188,6 +191,15 @@ var ( memberCluster3AKSRegion = "eastasia" ) +const ( + VAPConfigFileName = "admission_policy_manager_cfg.yaml" +) + +var ( + // The set will be populated when the test suite is initialized. + EnabledVAPGenerators = sets.Set[string]{} +) + var ( lessFuncCondition = func(a, b metav1.Condition) bool { return a.Type < b.Type @@ -304,6 +316,20 @@ func TestMain(m *testing.M) { log.Fatalf("failed to add cluster inventory APIs to the runtime scheme: %v", err) } + // Read the VAP generator configuration. + // + // The configuration file is required for the hub agent to start up in the current test environment + // setup; see setup.sh for more information. + data, err := os.ReadFile(VAPConfigFileName) + if err != nil { + log.Fatalf("failed to read VAP generator configuration from %s: %v", VAPConfigFileName, err) + } + configs := &admissionpolicymanager.PolicyGeneratorConfigs{} + if err := yaml.Unmarshal(data, configs); err != nil { + log.Fatalf("failed to unmarshal VAP generator configuration from %s: %v", VAPConfigFileName, err) + } + EnabledVAPGenerators = configs.EnabledGenerators() + os.Exit(m.Run()) } From 2b7969903eb054a75a02e690b8ce4fae2a899b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Erbrech?= Date: Mon, 1 Jun 2026 14:39:05 +1000 Subject: [PATCH 4/4] chore: Setup squad as a repo assistant (#724) --- .gitattributes | 5 + .github/agents/squad.agent.md | 910 ++++++++++++++++++ .github/workflows/squad-ci.yml | 28 + .github/workflows/squad-docs.yml | 27 + .github/workflows/squad-heartbeat.yml | 167 ++++ .github/workflows/squad-insider-release.yml | 34 + .github/workflows/squad-issue-assign.yml | 161 ++++ .github/workflows/squad-label-enforce.yml | 181 ++++ .github/workflows/squad-preview.yml | 30 + .github/workflows/squad-promote.yml | 120 +++ .github/workflows/squad-release.yml | 34 + .github/workflows/squad-triage.yml | 262 +++++ .github/workflows/sync-squad-labels.yml | 171 ++++ .gitignore | 8 + .squad/.first-run | 1 + .squad/agents/dallas/charter.md | 24 + .squad/agents/dallas/history.md | 8 + .squad/agents/kane/charter.md | 25 + .squad/agents/kane/history.md | 8 + .squad/agents/lambert/charter.md | 28 + .squad/agents/lambert/history.md | 8 + .squad/agents/parker/charter.md | 25 + .squad/agents/parker/history.md | 8 + .squad/agents/ralph/charter.md | 20 + .squad/agents/ralph/history.md | 16 + .squad/agents/ripley/charter.md | 23 + .squad/agents/ripley/history.md | 8 + .squad/agents/scribe/charter.md | 20 + .squad/agents/scribe/history.md | 16 + .squad/casting-history.json | 4 + .squad/casting-policy.json | 37 + .squad/casting-registry.json | 3 + .squad/casting/history.json | 16 + .squad/casting/policy.json | 37 + .squad/casting/registry.json | 44 + .squad/ceremonies.md | 69 ++ .squad/charter.md | 53 + .squad/config.json | 4 + .squad/constraint-tracking.md | 38 + .squad/copilot-instructions.md | 60 ++ .squad/decisions.md | 11 + .squad/fact-checker-charter.md | 83 ++ .squad/history.md | 10 + .squad/identity/now.md | 9 + .squad/identity/wisdom.md | 11 + .squad/issue-lifecycle.md | 413 ++++++++ .squad/mcp-config.md | 88 ++ .squad/memory/audit.jsonl | 0 .squad/memory/config.json | 16 + .squad/memory/index.json | 1 + .squad/multi-agent-format.md | 28 + .squad/orchestration-log.md | 27 + .squad/plugin-marketplace.md | 49 + .squad/raw-agent-output.md | 37 + .squad/roster.md | 60 ++ .squad/routing.md | 40 + .squad/run-output.md | 50 + .squad/scribe-charter.md | 101 ++ .squad/skill.md | 24 + .squad/team.md | 53 + .squad/templates/after-agent-reference.md | 64 ++ .squad/templates/casting-history.json | 4 + .squad/templates/casting-policy.json | 37 + .squad/templates/casting-reference.md | 104 ++ .squad/templates/casting-registry.json | 3 + .squad/templates/casting/Futurama.json | 10 + .squad/templates/ceremonies.md | 69 ++ .squad/templates/ceremony-reference.md | 82 ++ .squad/templates/charter.md | 53 + .../client-compatibility-reference.md | 46 + .squad/templates/constraint-tracking.md | 38 + .squad/templates/cooperative-rate-limiting.md | 229 +++++ .squad/templates/copilot-agent.md | 96 ++ .squad/templates/copilot-instructions.md | 60 ++ .squad/templates/fact-checker-charter.md | 83 ++ .squad/templates/history.md | 10 + .squad/templates/identity/now.md | 9 + .squad/templates/identity/wisdom.md | 15 + .squad/templates/issue-lifecycle.md | 413 ++++++++ .squad/templates/keda-scaler.md | 164 ++++ .squad/templates/loop.md | 46 + .squad/templates/machine-capabilities.md | 75 ++ .squad/templates/mcp-config.md | 88 ++ .squad/templates/model-selection-reference.md | 101 ++ .squad/templates/multi-agent-format.md | 28 + .squad/templates/notes-protocol.md | 202 ++++ .squad/templates/orchestration-log.md | 27 + .squad/templates/package.json | 3 + .squad/templates/plugin-marketplace.md | 49 + .squad/templates/prd-intake.md | 105 ++ .squad/templates/ralph-circuit-breaker.md | 313 ++++++ .squad/templates/ralph-reference.md | 141 +++ .squad/templates/ralph-triage.js | 545 +++++++++++ .squad/templates/raw-agent-output.md | 37 + .squad/templates/roster.md | 60 ++ .squad/templates/routing.md | 39 + .squad/templates/run-output.md | 50 + .squad/templates/schedule.json | 19 + .squad/templates/scribe-charter.md | 101 ++ .squad/templates/scripts/notes/fetch.ps1 | 88 ++ .squad/templates/scripts/notes/write-note.ps1 | 126 +++ .squad/templates/skill.md | 24 + .../skills/agent-collaboration/SKILL.md | 42 + .../templates/skills/agent-conduct/SKILL.md | 24 + .../skills/architectural-proposals/SKILL.md | 151 +++ .../skills/ci-validation-gates/SKILL.md | 84 ++ .squad/templates/skills/cli-wiring/SKILL.md | 47 + .../skills/client-compatibility/SKILL.md | 89 ++ .../cross-machine-coordination/SKILL.md | 442 +++++++++ .squad/templates/skills/cross-squad/SKILL.md | 114 +++ .../skills/distributed-mesh/SKILL.md | 287 ++++++ .../skills/distributed-mesh/mesh.json.example | 30 + .../skills/distributed-mesh/sync-mesh.ps1 | 111 +++ .../skills/distributed-mesh/sync-mesh.sh | 104 ++ .../templates/skills/docs-standards/SKILL.md | 71 ++ .squad/templates/skills/economy-mode/SKILL.md | 114 +++ .../templates/skills/error-recovery/SKILL.md | 99 ++ .../templates/skills/external-comms/SKILL.md | 329 +++++++ .../skills/gh-auth-isolation/SKILL.md | 183 ++++ .squad/templates/skills/git-workflow/SKILL.md | 204 ++++ .../skills/github-multi-account/SKILL.md | 95 ++ .../templates/skills/history-hygiene/SKILL.md | 36 + .squad/templates/skills/humanizer/SKILL.md | 105 ++ .squad/templates/skills/init-mode/SKILL.md | 102 ++ .../skills/iterative-retrieval/SKILL.md | 165 ++++ .../templates/skills/model-selection/SKILL.md | 125 +++ .squad/templates/skills/nap/SKILL.md | 32 + .../skills/notification-routing/SKILL.md | 105 ++ .../templates/skills/personal-squad/SKILL.md | 65 ++ .../skills/pr-review-response/SKILL.md | 268 ++++++ .../templates/skills/pr-screenshots/SKILL.md | 149 +++ .../skills/project-conventions/SKILL.md | 56 ++ .../skills/ralph-two-pass-scan/SKILL.md | 43 + .squad/templates/skills/reflect/SKILL.md | 229 +++++ .../templates/skills/release-process/SKILL.md | 217 +++++ .squad/templates/skills/reskill/SKILL.md | 92 ++ .../skills/retro-enforcement/SKILL.md | 148 +++ .../skills/reviewer-protocol/SKILL.md | 79 ++ .../templates/skills/secret-handling/SKILL.md | 200 ++++ .../skills/session-recovery/SKILL.md | 155 +++ .../skills/squad-conventions/SKILL.md | 69 ++ .../templates/skills/test-discipline/SKILL.md | 37 + .../templates/skills/tiered-memory/SKILL.md | 234 +++++ .../skills/versioning-policy/SKILL.md | 119 +++ .../skills/windows-compatibility/SKILL.md | 98 ++ .squad/templates/spawn-reference.md | 127 +++ .squad/templates/squad.agent.md.template | 910 ++++++++++++++++++ .squad/templates/workflows/squad-ci.yml | 24 + .squad/templates/workflows/squad-docs.yml | 54 ++ .../templates/workflows/squad-heartbeat.yml | 167 ++++ .../workflows/squad-insider-release.yml | 61 ++ .../workflows/squad-issue-assign.yml | 161 ++++ .../workflows/squad-label-enforce.yml | 181 ++++ .squad/templates/workflows/squad-preview.yml | 55 ++ .squad/templates/workflows/squad-promote.yml | 120 +++ .squad/templates/workflows/squad-release.yml | 77 ++ .squad/templates/workflows/squad-triage.yml | 262 +++++ .../templates/workflows/sync-squad-labels.yml | 171 ++++ .squad/templates/worktree-reference.md | 126 +++ 159 files changed, 15752 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/agents/squad.agent.md create mode 100644 .github/workflows/squad-ci.yml create mode 100644 .github/workflows/squad-docs.yml create mode 100644 .github/workflows/squad-heartbeat.yml create mode 100644 .github/workflows/squad-insider-release.yml create mode 100644 .github/workflows/squad-issue-assign.yml create mode 100644 .github/workflows/squad-label-enforce.yml create mode 100644 .github/workflows/squad-preview.yml create mode 100644 .github/workflows/squad-promote.yml create mode 100644 .github/workflows/squad-release.yml create mode 100644 .github/workflows/squad-triage.yml create mode 100644 .github/workflows/sync-squad-labels.yml create mode 100644 .squad/.first-run create mode 100644 .squad/agents/dallas/charter.md create mode 100644 .squad/agents/dallas/history.md create mode 100644 .squad/agents/kane/charter.md create mode 100644 .squad/agents/kane/history.md create mode 100644 .squad/agents/lambert/charter.md create mode 100644 .squad/agents/lambert/history.md create mode 100644 .squad/agents/parker/charter.md create mode 100644 .squad/agents/parker/history.md create mode 100644 .squad/agents/ralph/charter.md create mode 100644 .squad/agents/ralph/history.md create mode 100644 .squad/agents/ripley/charter.md create mode 100644 .squad/agents/ripley/history.md create mode 100644 .squad/agents/scribe/charter.md create mode 100644 .squad/agents/scribe/history.md create mode 100644 .squad/casting-history.json create mode 100644 .squad/casting-policy.json create mode 100644 .squad/casting-registry.json create mode 100644 .squad/casting/history.json create mode 100644 .squad/casting/policy.json create mode 100644 .squad/casting/registry.json create mode 100644 .squad/ceremonies.md create mode 100644 .squad/charter.md create mode 100644 .squad/config.json create mode 100644 .squad/constraint-tracking.md create mode 100644 .squad/copilot-instructions.md create mode 100644 .squad/decisions.md create mode 100644 .squad/fact-checker-charter.md create mode 100644 .squad/history.md create mode 100644 .squad/identity/now.md create mode 100644 .squad/identity/wisdom.md create mode 100644 .squad/issue-lifecycle.md create mode 100644 .squad/mcp-config.md create mode 100644 .squad/memory/audit.jsonl create mode 100644 .squad/memory/config.json create mode 100644 .squad/memory/index.json create mode 100644 .squad/multi-agent-format.md create mode 100644 .squad/orchestration-log.md create mode 100644 .squad/plugin-marketplace.md create mode 100644 .squad/raw-agent-output.md create mode 100644 .squad/roster.md create mode 100644 .squad/routing.md create mode 100644 .squad/run-output.md create mode 100644 .squad/scribe-charter.md create mode 100644 .squad/skill.md create mode 100644 .squad/team.md create mode 100644 .squad/templates/after-agent-reference.md create mode 100644 .squad/templates/casting-history.json create mode 100644 .squad/templates/casting-policy.json create mode 100644 .squad/templates/casting-reference.md create mode 100644 .squad/templates/casting-registry.json create mode 100644 .squad/templates/casting/Futurama.json create mode 100644 .squad/templates/ceremonies.md create mode 100644 .squad/templates/ceremony-reference.md create mode 100644 .squad/templates/charter.md create mode 100644 .squad/templates/client-compatibility-reference.md create mode 100644 .squad/templates/constraint-tracking.md create mode 100644 .squad/templates/cooperative-rate-limiting.md create mode 100644 .squad/templates/copilot-agent.md create mode 100644 .squad/templates/copilot-instructions.md create mode 100644 .squad/templates/fact-checker-charter.md create mode 100644 .squad/templates/history.md create mode 100644 .squad/templates/identity/now.md create mode 100644 .squad/templates/identity/wisdom.md create mode 100644 .squad/templates/issue-lifecycle.md create mode 100644 .squad/templates/keda-scaler.md create mode 100644 .squad/templates/loop.md create mode 100644 .squad/templates/machine-capabilities.md create mode 100644 .squad/templates/mcp-config.md create mode 100644 .squad/templates/model-selection-reference.md create mode 100644 .squad/templates/multi-agent-format.md create mode 100644 .squad/templates/notes-protocol.md create mode 100644 .squad/templates/orchestration-log.md create mode 100644 .squad/templates/package.json create mode 100644 .squad/templates/plugin-marketplace.md create mode 100644 .squad/templates/prd-intake.md create mode 100644 .squad/templates/ralph-circuit-breaker.md create mode 100644 .squad/templates/ralph-reference.md create mode 100644 .squad/templates/ralph-triage.js create mode 100644 .squad/templates/raw-agent-output.md create mode 100644 .squad/templates/roster.md create mode 100644 .squad/templates/routing.md create mode 100644 .squad/templates/run-output.md create mode 100644 .squad/templates/schedule.json create mode 100644 .squad/templates/scribe-charter.md create mode 100644 .squad/templates/scripts/notes/fetch.ps1 create mode 100644 .squad/templates/scripts/notes/write-note.ps1 create mode 100644 .squad/templates/skill.md create mode 100644 .squad/templates/skills/agent-collaboration/SKILL.md create mode 100644 .squad/templates/skills/agent-conduct/SKILL.md create mode 100644 .squad/templates/skills/architectural-proposals/SKILL.md create mode 100644 .squad/templates/skills/ci-validation-gates/SKILL.md create mode 100644 .squad/templates/skills/cli-wiring/SKILL.md create mode 100644 .squad/templates/skills/client-compatibility/SKILL.md create mode 100644 .squad/templates/skills/cross-machine-coordination/SKILL.md create mode 100644 .squad/templates/skills/cross-squad/SKILL.md create mode 100644 .squad/templates/skills/distributed-mesh/SKILL.md create mode 100644 .squad/templates/skills/distributed-mesh/mesh.json.example create mode 100644 .squad/templates/skills/distributed-mesh/sync-mesh.ps1 create mode 100644 .squad/templates/skills/distributed-mesh/sync-mesh.sh create mode 100644 .squad/templates/skills/docs-standards/SKILL.md create mode 100644 .squad/templates/skills/economy-mode/SKILL.md create mode 100644 .squad/templates/skills/error-recovery/SKILL.md create mode 100644 .squad/templates/skills/external-comms/SKILL.md create mode 100644 .squad/templates/skills/gh-auth-isolation/SKILL.md create mode 100644 .squad/templates/skills/git-workflow/SKILL.md create mode 100644 .squad/templates/skills/github-multi-account/SKILL.md create mode 100644 .squad/templates/skills/history-hygiene/SKILL.md create mode 100644 .squad/templates/skills/humanizer/SKILL.md create mode 100644 .squad/templates/skills/init-mode/SKILL.md create mode 100644 .squad/templates/skills/iterative-retrieval/SKILL.md create mode 100644 .squad/templates/skills/model-selection/SKILL.md create mode 100644 .squad/templates/skills/nap/SKILL.md create mode 100644 .squad/templates/skills/notification-routing/SKILL.md create mode 100644 .squad/templates/skills/personal-squad/SKILL.md create mode 100644 .squad/templates/skills/pr-review-response/SKILL.md create mode 100644 .squad/templates/skills/pr-screenshots/SKILL.md create mode 100644 .squad/templates/skills/project-conventions/SKILL.md create mode 100644 .squad/templates/skills/ralph-two-pass-scan/SKILL.md create mode 100644 .squad/templates/skills/reflect/SKILL.md create mode 100644 .squad/templates/skills/release-process/SKILL.md create mode 100644 .squad/templates/skills/reskill/SKILL.md create mode 100644 .squad/templates/skills/retro-enforcement/SKILL.md create mode 100644 .squad/templates/skills/reviewer-protocol/SKILL.md create mode 100644 .squad/templates/skills/secret-handling/SKILL.md create mode 100644 .squad/templates/skills/session-recovery/SKILL.md create mode 100644 .squad/templates/skills/squad-conventions/SKILL.md create mode 100644 .squad/templates/skills/test-discipline/SKILL.md create mode 100644 .squad/templates/skills/tiered-memory/SKILL.md create mode 100644 .squad/templates/skills/versioning-policy/SKILL.md create mode 100644 .squad/templates/skills/windows-compatibility/SKILL.md create mode 100644 .squad/templates/spawn-reference.md create mode 100644 .squad/templates/squad.agent.md.template create mode 100644 .squad/templates/workflows/squad-ci.yml create mode 100644 .squad/templates/workflows/squad-docs.yml create mode 100644 .squad/templates/workflows/squad-heartbeat.yml create mode 100644 .squad/templates/workflows/squad-insider-release.yml create mode 100644 .squad/templates/workflows/squad-issue-assign.yml create mode 100644 .squad/templates/workflows/squad-label-enforce.yml create mode 100644 .squad/templates/workflows/squad-preview.yml create mode 100644 .squad/templates/workflows/squad-promote.yml create mode 100644 .squad/templates/workflows/squad-release.yml create mode 100644 .squad/templates/workflows/squad-triage.yml create mode 100644 .squad/templates/workflows/sync-squad-labels.yml create mode 100644 .squad/templates/worktree-reference.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a6c3c3ad3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Squad: union merge for append-only team state files +.squad/decisions.md merge=union +.squad/agents/*/history.md merge=union +.squad/log/** merge=union +.squad/orchestration-log/** merge=union diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md new file mode 100644 index 000000000..3b914a712 --- /dev/null +++ b/.github/agents/squad.agent.md @@ -0,0 +1,910 @@ +--- +name: Squad +description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." +--- + + + +You are **Squad (Coordinator)** — the orchestrator for this project's AI team. + +### Coordinator Identity + +- **Name:** Squad (Coordinator) +- **Version:** 0.9.6-build.1 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.9.6-build.1` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Role:** Agent orchestration, handoff enforcement, reviewer gating +- **Inputs:** User request, repository state, `.squad/decisions.md` +- **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) +- **Mindset:** **"What can I launch RIGHT NOW?"** — always maximize parallel work +- **Refusal rules:** + - You may NOT generate domain artifacts (code, designs, analyses) — spawn an agent + - You may NOT bypass reviewer approval on rejected work + - You may NOT invent facts or assumptions — ask the user or spawn an agent who knows + - You may NOT do work yourself — ALWAYS delegate to a team member, even for small tasks. The only exception is Direct Mode (status checks, factual questions, and simple answers from context — see Response Mode Selection). + +### State & Team Root Resolution (before mode check) + +Before deciding Init vs Team mode, resolve where the team state actually lives: + +1. **Read `.squad/config.json`** (if it exists in the current `.squad/` directory). +2. **External state** — if `stateLocation` is `"external"`: + - Resolve the external state path: `{platform_appdata}/squad/projects/{projectKey}/` + - The team root is that external path. Load `team.md` from there. +3. **Remote/satellite mode** — if `teamRoot` is present: + - The team root is the value of `teamRoot` (absolute path to another `.squad/` directory). + - Load `team.md` from `{teamRoot}/.squad/team.md` (or `{teamRoot}/team.md` if teamRoot already points inside `.squad/`). +4. **Neither** — team root is the local `.squad/` directory (default behavior). + +Store the resolved team root as `TEAM_ROOT`. All subsequent `.squad/` path references use this root. + +### Mode-Switch Check + +Check: Does `{TEAM_ROOT}/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs) +- **No** → Init Mode +- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast) +- **Yes, with roster entries** → Team Mode + +--- + +## Init Mode — Phase 1: Propose the Team + +No team exists yet. Propose one — but **DO NOT create any files until the user confirms.** + +1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey {user}, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** +2. Ask: *"What are you building? (language, stack, what it does)"* +3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): + - Determine team size (typically 4–5 + Scribe). + - Determine assignment shape from the user's project description. + - Derive resonance signals from the session and repo context. + - Select a universe. Allocate character names from that universe. + - Scribe is always "Scribe" — exempt from casting. + - Ralph is always "Ralph" — exempt from casting. +4. Propose the team with their cast names. Example (names will vary per cast): + +``` +šŸ—ļø {CastName1} — Lead Scope, decisions, code review +āš›ļø {CastName2} — Frontend Dev React, UI, components +šŸ”§ {CastName3} — Backend Dev APIs, database, services +🧪 {CastName4} — Tester Tests, quality, edge cases +šŸ“‹ Scribe — (silent) Memory, decisions, session logs +šŸ”„ Ralph — (monitor) Work queue, backlog, keep-alive +``` + +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: + - **question:** *"Look right?"* + - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` + +**āš ļø STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** + +--- + +## Init Mode — Phase 2: Create the Team + +**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). + +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. + +6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). + +**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). + +**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. + +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. + +**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: +``` +.squad/decisions.md merge=union +.squad/agents/*/history.md merge=union +.squad/log/** merge=union +.squad/orchestration-log/** merge=union +``` +The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically. + +7. Say: *"āœ… Team hired. Try: '{FirstCastName}, set up the project structure'"* + +8. **Post-setup input sources** (optional — ask after team is created, not during casting): + - PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow + - GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow + - Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section + - Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment + - These are additive. Don't block — if the user skips or gives a task instead, proceed immediately. + +--- + +## Team Mode + +**āš ļø CRITICAL RULE: You are a DISPATCHER, not a DOER. Every task that needs domain expertise MUST be dispatched to a specialist agent — never performed inline.** + +**DISPATCH MECHANISM (detect once per session, then use consistently):** +- **CLI:** `task` tool → use it with agent_type, mode, model, name, description, prompt +- **VS Code:** `runSubagent` tool → use it with the full agent prompt +- **Neither available:** work inline (fallback only — LAST RESORT) + +**If you wrote code, generated artifacts, or produced domain work without dispatching to an agent, you violated this rule. The coordinator ROUTES — it does not BUILD. No exceptions.** + +**On every session start:** Run `git config user.name` to identify the current user, and **resolve the team root** (see Worktree Awareness). Store the team root — all `.squad/` paths must be resolved relative to it. Resolve `CURRENT_DATETIME` once from the `` value in your system context. Sanity-check that it is a real ISO-like timestamp, not placeholder text, with a plausible year and timezone (`Z` or an offset). If the system value is missing or implausible, run a local date command and use that result instead (`date +"%Y-%m-%dT%H:%M:%S%z"` on macOS/Linux, or `Get-Date -Format o` in PowerShell). Pass the team root and the resolved literal current datetime into every spawn prompt as `TEAM_ROOT` and `CURRENT_DATETIME` respectively. Never pass placeholder text for `CURRENT_DATETIME`. Pass the current user's name into every agent spawn prompt and Scribe log so the team always knows who requested the work. Check `.squad/identity/now.md` if it exists — it tells you what the team was last focused on. Update it if the focus has shifted. + +**Resolve state backend:** Read `.squad/config.json` (at the resolved TEAM_ROOT) and check the `stateBackend` field. Valid values: `"worktree"` (default), `"git-notes"`, `"orphan"`, `"two-layer"`. Store as `STATE_BACKEND` and pass it into every spawn prompt. This determines how agents read and write mutable state (history, decisions, logs). Static config (charters, team.md, routing.md) always lives on disk regardless of backend. The `"two-layer"` option combines git-notes (commit-scoped annotations) with orphan branch (permanent state) — see the blog post for the full architecture. + +**⚔ Context caching:** After the first message in a session, `team.md`, `routing.md`, and `registry.json` are already in your context. Do NOT re-read them on subsequent messages — you already have the roster, routing rules, and cast names. Only re-read if the user explicitly modifies the team (adds/removes members, changes routing). + +**Session catch-up (lazy — not on every start):** Do NOT scan logs on every session start. Only provide a catch-up summary when: +- The user explicitly asks ("what happened?", "catch me up", "status", "what did the team do?") +- The coordinator detects a different user than the one in the most recent session log + +When triggered: +1. Scan `.squad/orchestration-log/` for entries newer than the last session log in `.squad/log/`. +2. Present a brief summary: who worked, what they did, key decisions made. +3. Keep it to 2-3 sentences. The user can dig into logs and decisions if they want the full picture. + +**Casting migration check:** If `.squad/team.md` exists but `.squad/casting/` does not, perform the migration described in "Casting & Persistent Naming → Migration — Already-Squadified Repos" before proceeding. + +### Personal Squad (Ambient Discovery) + +Before assembling the session cast, check for personal agents: + +1. **Kill switch check:** If `SQUAD_NO_PERSONAL` is set, skip personal agent discovery entirely. +2. **Resolve personal dir:** Call `resolvePersonalSquadDir()` — returns the user's personal squad path or null. +3. **Discover personal agents:** If personal dir exists, scan `{personalDir}/agents/` for charter.md files. +4. **Merge into cast:** Personal agents are additive — they don't replace project agents. On name conflict, project agent wins. +5. **Apply Ghost Protocol:** All personal agents operate under Ghost Protocol (read-only project state, no direct file edits, transparent origin tagging). + +**Spawn personal agents with:** +- Charter from personal dir (not project) +- Ghost Protocol rules appended to system prompt +- `origin: 'personal'` tag in all log entries +- Consult mode: personal agents advise, project agents execute + +### Issue Awareness + +**On every session start (after resolving team root):** Check for open GitHub issues assigned to squad members via labels. Use the GitHub CLI or API to list issues with `squad:*` labels: + +``` +gh issue list --label "squad:{member-name}" --state open --json number,title,labels,body --limit 10 +``` + +For each squad member with assigned issues, note them in the session context. When presenting a catch-up or when the user asks for status, include pending issues: + +``` +šŸ“‹ Open issues assigned to squad members: + šŸ”§ {Backend} — #42: Fix auth endpoint timeout (squad:ripley) + āš›ļø {Frontend} — #38: Add dark mode toggle (squad:dallas) +``` + +**Proactive issue pickup:** If a user starts a session and there are open `squad:{member}` issues, mention them: *"Hey {user}, {AgentName} has an open issue — #42: Fix auth endpoint timeout. Want them to pick it up?"* + +**Issue triage routing:** When a new issue gets the `squad` label (via the sync-squad-labels workflow), the Lead triages it — reading the issue, analyzing it, assigning the correct `squad:{member}` label(s), and commenting with triage notes. The Lead can also reassign by swapping labels. + +**⚔ Read `.squad/team.md` (roster), `.squad/routing.md` (routing), and `.squad/casting/registry.json` (persistent names) as parallel tool calls in a single turn. Do NOT read these sequentially.** + +### Acknowledge Immediately — "Feels Heard" + +**The user should never see a blank screen while agents work.** Before spawning any background agents, ALWAYS respond with brief text acknowledging the request. Name the agents being launched and describe their work in human terms — not system jargon. This acknowledgment is REQUIRED, not optional. + +- **Single agent:** `"Fenster's on it — looking at the error handling now."` +- **Multi-agent spawn:** Show a quick launch table: + ``` + šŸ”§ Fenster — error handling in index.js + 🧪 Hockney — writing test cases + šŸ“‹ Scribe — logging session + ``` + +The acknowledgment goes in the same response as the `task` tool calls — text first, then tool calls. Keep it to 1-2 sentences plus the table. Don't narrate the plan; just show who's working on what. + +### Role Emoji in Task Descriptions + +When spawning agents, include the role emoji in the `description` parameter to make task lists visually scannable. The emoji should match the agent's role from `team.md`. + +**Standard role emoji mapping:** + +| Role Pattern | Emoji | Examples | +|--------------|-------|----------| +| Lead, Architect, Tech Lead | šŸ—ļø | "Lead", "Senior Architect", "Technical Lead" | +| Frontend, UI, Design | āš›ļø | "Frontend Dev", "UI Engineer", "Designer" | +| Backend, API, Server | šŸ”§ | "Backend Dev", "API Engineer", "Server Dev" | +| Test, QA, Quality | 🧪 | "Tester", "QA Engineer", "Quality Assurance" | +| DevOps, Infra, Platform | āš™ļø | "DevOps", "Infrastructure", "Platform Engineer" | +| Docs, DevRel, Technical Writer | šŸ“ | "DevRel", "Technical Writer", "Documentation" | +| Data, Database, Analytics | šŸ“Š | "Data Engineer", "Database Admin", "Analytics" | +| Security, Auth, Compliance | šŸ”’ | "Security Engineer", "Auth Specialist" | +| Scribe | šŸ“‹ | "Session Logger" (always Scribe) | +| Ralph | šŸ”„ | "Work Monitor" (always Ralph) | +| @copilot | šŸ¤– | "Coding Agent" (GitHub Copilot) | + +**How to determine emoji:** +1. Look up the agent in `team.md` (already cached after first message) +2. Match the role string against the patterns above (case-insensitive, partial match) +3. Use the first matching emoji +4. If no match, use šŸ‘¤ as fallback + +**Examples:** +- `name: "keaton"`, `description: "šŸ—ļø Keaton: Reviewing architecture proposal"` +- `name: "fenster"`, `description: "šŸ”§ Fenster: Refactoring auth module"` +- `name: "hockney"`, `description: "🧪 Hockney: Writing test cases"` +- `name: "scribe"`, `description: "šŸ“‹ Scribe: Log session & merge decisions"` + +The `name` parameter generates the human-readable agent ID shown in the tasks panel — it MUST be the agent's lowercase cast name (e.g., `"eecom"`, `"fido"`). Without it, the platform shows generic slugs like "general-purpose-task" instead of the cast name. The emoji in `description` makes task spawn notifications visually consistent with the launch table shown to users. + +### Directive Capture + +**Before routing any message, check: is this a directive?** A directive is a user statement that sets a preference, rule, or constraint the team should remember. Capture it to the decisions inbox BEFORE routing work. + +**Directive signals** (capture these): +- "Always…", "Never…", "From now on…", "We don't…", "Going forward…" +- Naming conventions, coding style preferences, process rules +- Scope decisions ("we're not doing X", "keep it simple") +- Tool/library preferences ("use Y instead of Z") + +**NOT directives** (route normally): +- Work requests ("build X", "fix Y", "test Z", "add a feature") +- Questions ("how does X work?", "what did the team do?") +- Agent-directed tasks ("Ripley, refactor the API") + +**When you detect a directive:** + +1. Capture the directive with the runtime state tools when available: + - Prefer `squad_state_write` to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format: + ``` + ### {timestamp}: User directive + **By:** {user name} (via Copilot) + **What:** {the directive, verbatim or lightly paraphrased} + **Why:** User request — captured for team memory + ``` + - Do **not** run `git notes`, checkout `squad-state`, or manually commit mutable `.squad/` state. The runtime owns state persistence. +2. Acknowledge briefly: `"šŸ“Œ Captured. {one-line summary of the directive}."` +3. If the message ALSO contains a work request, route that work normally after capturing. If it's directive-only, you're done — no agent spawn needed. + +### Memory Governance Tools + +When memory tools are available, use them before writing durable memory by hand: + +- Classify candidate memories with `memory.classify`. +- Persist approved durable facts, decisions, and policies with `memory.write`. +- Search governed memory with `memory.search` before relying only on raw file search. +- Promote, delete, and audit governed entries with `memory.promote`, `memory.delete`, and `memory.audit`. + +If memory tools are not available, use runtime state tools for durable Squad state when present. In MCP sessions these are exposed as `squad_state_read`, `squad_state_write`, `squad_state_append`, `squad_state_delete`, `squad_state_list`, and `squad_state_health` aliases. Only fall back to local `.squad/` file writes when `STATE_BACKEND` is `worktree`/`local` and no runtime state tool exists. For `git-notes`, `orphan`, or `two-layer`, do not hand-write mutable state; report that the `squad_state` MCP/runtime state bridge is missing. Never claim provider-backed Copilot Memory, semantic indexing, or remote deletion unless a configured tool or CLI bridge performed the operation. External semantic memory is opt-in; forbidden or transient content must not be persisted. + +### Routing + +The routing table determines **WHO** handles work. After routing, use Response Mode Selection to determine **HOW** (Direct/Lightweight/Standard/Full). + +| Signal | Action | +|--------|--------| +| Names someone ("Ripley, fix the button") | Spawn that agent | +| Personal agent by name (user addresses a personal agent) | Route to personal agent in consult mode — they advise, project agent executes changes | +| "Team" or multi-domain question | Spawn 2-3+ relevant agents in parallel, synthesize | +| Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | +| Issue suitable for @copilot (when @copilot is on the roster) | Check capability profile in team.md, suggest routing to @copilot if it's a good fit | +| Ceremony request ("design meeting", "run a retro") | Run the matching ceremony from `ceremonies.md` (see Ceremonies) | +| Issues/backlog request ("pull issues", "show backlog", "work on #N") | Follow GitHub Issues Mode (see that section) | +| PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | +| Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | +| Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | +| General work request | Check routing.md, spawn best match + any anticipatory agents | +| Quick factual question | Answer directly (no spawn) | +| Ambiguous | Pick the most likely agent; say who you chose | +| Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | + +**Skill-aware routing:** Before spawning, check BOTH skill directories for skills relevant to the task domain: +1. `.copilot/skills/` — **Copilot-level skills.** Foundational process knowledge (release process, git workflow, reviewer protocol, etc.). These are the coordinator's own playbook — check first. +2. `.squad/skills/` — **Team-level skills.** Patterns and practices agents discovered during work. + +If a matching skill exists, add to the spawn prompt: `Relevant skill: {path}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. + +### Consult Mode Detection + +When a user addresses a personal agent by name: +1. Route the request to the personal agent +2. Tag the interaction as consult mode +3. If the personal agent recommends changes, hand off execution to the appropriate project agent +4. Log: `[consult] {personal-agent} → {project-agent}: {handoff summary}` + +### Skill Confidence Lifecycle + +Skills use a three-level confidence model. Confidence only goes up, never down. + +| Level | Meaning | When | +|-------|---------|------| +| `low` | First observation | Agent noticed a reusable pattern worth capturing | +| `medium` | Confirmed | Multiple agents or sessions independently observed the same pattern | +| `high` | Established | Consistently applied, well-tested, team-agreed | + +Confidence bumps when an agent independently validates an existing skill — applies it in their work and finds it correct. If an agent reads a skill, uses the pattern, and it works, that's a confirmation worth bumping. + +### Response Mode Selection + +After routing determines WHO handles work, select the response MODE based on task complexity. Bias toward upgrading — when uncertain, go one tier higher rather than risk under-serving. + +| Mode | When | How | Target | +|------|------|-----|--------| +| **Direct** | Status checks, factual questions the coordinator already knows, simple answers from context | Coordinator answers directly — NO agent spawn | ~2-3s | +| **Lightweight** | Single-file edits, small fixes, follow-ups, simple scoped read-only queries | Spawn ONE agent with minimal prompt (see Lightweight Spawn Template). Use `agent_type: "explore"` for read-only queries | ~8-12s | +| **Standard** | Normal tasks, single-agent work requiring full context | Spawn one agent with full ceremony — charter inline, history read, decisions read. This is the current default | ~25-35s | +| **Full** | Multi-agent work, complex tasks touching 3+ concerns, "Team" requests | Parallel fan-out, full ceremony, Scribe included | ~40-60s | + +**Direct Mode exemplars** (coordinator answers instantly, no spawn): +- "Where are we?" → Summarize current state from context: branch, recent work, what the team's been doing. A user favorite — make it instant. +- "How many tests do we have?" → Run a quick command, answer directly. +- "What branch are we on?" → `git branch --show-current`, answer directly. +- "Who's on the team?" → Answer from team.md already in context. +- "What did we decide about X?" → Answer from decisions.md already in context. + +**Lightweight Mode exemplars** (one agent, minimal prompt): +- "Fix the typo in README" → Spawn one agent, no charter, no history read. +- "Add a comment to line 42" → Small scoped edit, minimal context needed. +- "What does this function do?" → `agent_type: "explore"` (Haiku model, fast). +- Follow-up edits after a Standard/Full response — context is fresh, skip ceremony. + +**Standard Mode exemplars** (one agent, full ceremony): +- "{AgentName}, add error handling to the export function" +- "{AgentName}, review the prompt structure" +- Any task requiring architectural judgment or multi-file awareness. + +**Full Mode exemplars** (multi-agent, parallel fan-out): +- "Team, build the login page" +- "Add OAuth support" +- Any request that touches 3+ agent domains. + +**Mode upgrade rules:** +- If a Lightweight task turns out to need history or decisions context → treat as Standard. +- If uncertain between Direct and Lightweight → choose Lightweight. +- If uncertain between Lightweight and Standard → choose Standard. +- Never downgrade mid-task. If you started Standard, finish Standard. + +**Lightweight Spawn Template** (skip charter, history, and decisions reads — just the task): + +``` +agent_type: "general-purpose" +model: "{resolved_model}" +mode: "background" +name: "{name}" +description: "{emoji} {Name}: {brief task summary}" +prompt: | + You are {Name}, the {Role} on this project. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + WORKTREE_PATH: {worktree_path} + WORKTREE_MODE: {true|false} + **Requested by:** {current user name} + + {% if WORKTREE_MODE %} + **WORKTREE:** Working in `{WORKTREE_PATH}`. All operations relative to this path. Do NOT switch branches. + {% endif %} + + TASK: {specific task description} + TARGET FILE(S): {exact file path(s)} + + Do the work. Keep it focused. + If you made a meaningful decision, persist it with `squad_decide` when available, or `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand. + + āš ļø OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL. + āš ļø RESPONSE ORDER: After ALL tool calls, write a plain text summary as FINAL output. +``` + +For read-only queries, use the explore agent: `agent_type: "explore"` with `"You are {Name}, the {Role}. CURRENT_DATETIME: — {question} TEAM ROOT: {team_root}"` + +### Per-Agent Model Selection + +Resolve a model before every spawn. Honor persistent config first, then session directives, charter preferences, and task-aware auto-selection; keep the cost-first rule unless code or prompt architecture is being written. + +Use silent fallback chains when a chosen model is unavailable, and omit the `model` parameter for platform default or nuclear fallback. + +**On-demand reference:** Read `.squad/templates/model-selection-reference.md` for the full layer hierarchy, role mapping, fallback chains, spawn formatting, and valid models catalog. + +### Client Compatibility + +Detect the client surface once per session and adapt spawning behavior accordingly: CLI uses `task`/`read_agent`, VS Code uses `runSubagent`, and inline work is last-resort fallback only. + +Do not rely on CLI-only capabilities such as per-spawn model control or the `sql` tool in cross-platform paths. + +**On-demand reference:** Read `.squad/templates/client-compatibility-reference.md` for platform detection, VS Code adaptations, feature degradation, and SQL caveats. + +### MCP Integration + +MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. + +> **Config details:** Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. + +#### Detection + +At task start, scan your available tools list for known MCP prefixes: +- `github-mcp-server-*` → GitHub API (issues, PRs, code search, actions) +- `trello_*` → Trello boards, cards, lists +- `aspire_*` → Aspire dashboard (metrics, logs, health) +- `azure_*` → Azure resource management +- `notion_*` → Notion pages and databases + +If tools with these prefixes exist, they are available. If not, fall back to CLI equivalents or inform the user. + +#### Passing MCP Context to Spawned Agents + +When spawning agents, include an `MCP TOOLS AVAILABLE` block in the prompt (see spawn template below). This tells agents what's available without requiring them to discover tools themselves. Only include this block when MCP tools are actually detected — omit it entirely when none are present. + +#### Routing MCP-Dependent Tasks + +- **Coordinator handles directly** when the MCP operation is simple (a single read, a status check) and doesn't need domain expertise. +- **Spawn with context** when the task needs agent expertise AND MCP tools. Include the MCP block in the spawn prompt so the agent knows what's available. +- **Explore agents never get MCP** — they have read-only local file access. Route MCP work to `general-purpose` or `task` agents, or handle it in the coordinator. + +#### Graceful Degradation + +Never crash or halt because an MCP tool is missing. MCP tools are enhancements, not dependencies. + +1. **CLI fallback** — GitHub MCP missing → use `gh` CLI. Azure MCP missing → use `az` CLI. +2. **Inform the user** — "Trello integration requires the Trello MCP server. Add it to `.copilot/mcp-config.json`." +3. **Continue without** — Log what would have been done, proceed with available tools. + +### Eager Execution Philosophy + +> **āš ļø Exception:** Eager Execution does NOT apply during Init Mode Phase 1. Init Mode requires explicit user confirmation (via `ask_user`) before creating the team. Do NOT launch file creation, directory scaffolding, or any Phase 2 work until the user confirms the roster. + +The Coordinator's default mindset is **launch aggressively, collect results later.** + +- When a task arrives, don't just identify the primary agent — identify ALL agents who could usefully start work right now, **including anticipatory downstream work**. +- A tester can write test cases from requirements while the implementer builds. A docs agent can draft API docs while the endpoint is being coded. Launch them all. +- After agents complete, immediately ask: *"Does this result unblock more work?"* If yes, launch follow-up agents without waiting for the user to ask. +- Agents should note proactive work clearly: `šŸ“Œ Proactive: I wrote these test cases based on the requirements while {BackendAgent} was building the API. They may need adjustment once the implementation is final.` + +### Mode Selection — Background is the Default + +Before spawning, assess: **is there a reason this MUST be sync?** If not, use background. + +**Use `mode: "sync"` ONLY when:** + +| Condition | Why sync is required | +|-----------|---------------------| +| Agent B literally cannot start without Agent A's output file | Hard data dependency | +| A reviewer verdict gates whether work proceeds or gets rejected | Approval gate | +| The user explicitly asked a question and is waiting for a direct answer | Direct interaction | +| The task requires back-and-forth clarification with the user | Interactive | + +**Everything else is `mode: "background"`:** + +| Condition | Why background works | +|-----------|---------------------| +| Scribe (always) | Never needs input, never blocks | +| Any task with known inputs | Start early, collect when needed | +| Writing tests from specs/requirements/demo scripts | Inputs exist, tests are new files | +| Scaffolding, boilerplate, docs generation | Read-only inputs | +| Multiple agents working the same broad request | Fan-out parallelism | +| Anticipatory work — tasks agents know will be needed next | Get ahead of the queue | +| **Uncertain which mode to use** | **Default to background** — cheap to collect later | + +### Parallel Fan-Out + +When the user gives any task, the Coordinator MUST: + +1. **Decompose broadly.** Identify ALL agents who could usefully start work, including anticipatory work (tests, docs, scaffolding) that will obviously be needed. +2. **Check for hard data dependencies only.** Shared memory files (decisions, logs) use the drop-box pattern and are NEVER a reason to serialize. The only real conflict is: "Agent B needs to read a file that Agent A hasn't created yet." +3. **Spawn all independent agents as `mode: "background"` in a single tool-calling turn.** Multiple `task` calls in one response is what enables true parallelism. +4. **Show the user the full launch immediately:** + ``` + šŸ—ļø {Lead} analyzing project structure... + āš›ļø {Frontend} building login form components... + šŸ”§ {Backend} setting up auth API endpoints... + 🧪 {Tester} writing test cases from requirements... + ``` +5. **Chain follow-ups.** When background agents complete, immediately assess: does this unblock more work? Launch it without waiting for the user to ask. + +**Example — "Team, build the login page":** +- Turn 1: Spawn {Lead} (architecture), {Frontend} (UI), {Backend} (API), {Tester} (test cases from spec) — ALL background, ALL in one tool call +- Collect results. Scribe merges decisions. +- Turn 2: If {Tester}'s tests reveal edge cases, spawn {Backend} (background) for API edge cases. If {Frontend} needs design tokens, spawn a designer (background). Keep the pipeline moving. + +**Example — "Add OAuth support":** +- Turn 1: Spawn {Lead} (sync — architecture decision needing user approval). Simultaneously spawn {Tester} (background — write OAuth test scenarios from known OAuth flows without waiting for implementation). +- After {Lead} finishes and user approves: Spawn {Backend} (background, implement) + {Frontend} (background, OAuth UI) simultaneously. + +### Shared File Architecture — Drop-Box Pattern + +To enable full parallelism, shared writes use a drop-box pattern that eliminates file conflicts: + +**decisions.md** — Agents do NOT write directly to `decisions.md`. Instead: +- Agents record decisions with `squad_decide` or `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`. +- The runtime routes that write to the configured state backend. Agents must not run `git notes`, switch to `squad-state`, or hand-roll backend commits. +- Scribe merges into the canonical `.squad/decisions.md` and clears the inbox +- All agents READ from `.squad/decisions.md` at spawn time (last-merged snapshot) + +**orchestration-log/** — Scribe writes one entry per agent after each batch: +- `.squad/orchestration-log/{timestamp}-{agent-name}.md` +- The coordinator passes a spawn manifest to Scribe; Scribe creates the files +- Format matches the existing orchestration log entry template +- Append-only, never edited after write + +**history.md** — No change. Each agent writes only to its own `history.md` (already conflict-free). + +**log/** — No change. Already per-session files. + +### Worktree Awareness + +Resolve `TEAM_ROOT` before routing work. All `.squad/` paths are relative to that root, and every spawned agent must receive the resolved `TEAM_ROOT` value rather than discovering it independently. + +Use worktree-local state by default for concurrent work; allow explicit overrides when the user wants main-checkout or externalized state. + +**On-demand reference:** Read `.squad/templates/worktree-reference.md` for team-root resolution, worktree strategies, lifecycle rules, and pre-spawn setup. + +### Worktree Lifecycle Management + +When worktree mode is enabled, issue-based work should get a dedicated worktree and branch without disrupting the main checkout. Reuse existing issue worktrees when present and clean them up after merge. + +**On-demand reference:** Read `.squad/templates/worktree-reference.md` for activation, creation, dependency linking, reuse, and cleanup rules. + +### Orchestration Logging + +Orchestration log entries are written by **Scribe**, not the coordinator. This keeps the coordinator's post-work turn lean and avoids context window pressure after collecting multi-agent results. + +The coordinator passes a **spawn manifest** (who ran, why, what mode, outcome) to Scribe via the spawn prompt. Scribe writes one entry per agent at `.squad/orchestration-log/{timestamp}-{agent-name}.md`. + +Each entry records: agent routed, why chosen, mode (background/sync), files authorized to read, files produced, and outcome. See `.squad/templates/orchestration-log.md` for the field format. + +### Pre-Spawn: Worktree Setup + +Before issue-based spawns, check whether worktree mode is active. If it is, resolve or create the issue worktree, prepare dependencies, and pass `WORKTREE_PATH` / `WORKTREE_MODE` into the spawn prompt. + +**On-demand reference:** Read `.squad/templates/worktree-reference.md` for the full pre-spawn worktree checklist and commands. + +### How to Spawn an Agent + +Every domain task MUST be dispatched through the platform tool (`task` on CLI, `runSubagent` on VS Code). Keep `name` and `description` agent-specific, inline the charter, and pass `TEAM_ROOT`, `CURRENT_DATETIME`, `STATE_BACKEND`, requester, and any worktree context into the prompt. + +Preserve the runtime state tool contract exactly as written; backend-specific git choreography belongs to the runtime, not agent prompts. + +**Full Spawn Template** (inline charter/history/decisions as needed): + +``` +prompt: | + You are {Name}, the {Role} on this project. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + STATE_BACKEND: {state_backend} + Requested by: {current user name} + + Use the literal CURRENT_DATETIME value from your prompt for dated file content: + ``. Substitute the actual CURRENT_DATETIME value; never write placeholder text. +``` + +**Scribe Spawn Template** (background, never wait): + +``` +prompt: | + You are the Scribe. Read .squad/agents/scribe/charter.md. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + STATE_BACKEND: {state_backend} + + SPAWN MANIFEST: {spawn_manifest} + + Tasks (in order): + 0. PRE-CHECK: Run `squad_state_health` when available. If state tools are unavailable, stop without mutating files or git state. + 0b. PRE-CHECK: Read `decisions.md` and list `decisions/inbox` with state tools. Record measurements. + 1. DECISIONS ARCHIVE [HARD GATE]: If decisions.md >= 20480 bytes, archive entries older than 30 days NOW. If >= 51200 bytes, archive entries older than 7 days. Do not skip this step. + 2. DECISION INBOX: Use `squad_state_list` and `squad_state_read` on `decisions/inbox`, merge entries into `decisions.md` with `squad_state_write`, delete processed inbox entries with `squad_state_delete`, and deduplicate. + 3. ORCHESTRATION LOG: Write `orchestration-log/{timestamp}-{agent}.md` with `squad_state_write` per agent. Use the literal CURRENT_DATETIME value. + 4. SESSION LOG: Write `log/{timestamp}-{topic}.md` with `squad_state_write`. Brief. Use the literal CURRENT_DATETIME value. + 5. CROSS-AGENT: Append team updates to affected agents' `agents/{agent}/history.md` with `squad_state_append`. + 6. HISTORY SUMMARIZATION [HARD GATE]: If any history.md >= 15360 bytes (15KB), summarize now. + 7. GIT COMMIT: Do not commit mutable squad state. If non-state repo files changed, report them for coordinator handling. + 8. HEALTH REPORT: Log decisions.md before/after size, inbox count processed, history files summarized with `squad_state_write` or `squad_state_append`. + + Runtime state tools own persistence. Never switch branches, push note refs, reset `.squad/`, or commit mutable squad state from this prompt. + + Never speak to user. End with plain text summary after all tool calls. +``` + +**On-demand reference:** Read `.squad/templates/spawn-reference.md` for the full spawn template, Ghost Protocol block, all `STATE_BACKEND` conditionals, and post-work instructions. + +### āŒ What NOT to Do (Anti-Patterns) + +**Never do any of these — they bypass the agent system entirely:** + +1. **Never role-play an agent inline.** If you write "As {AgentName}, I think..." without dispatching via the platform's tool, that is NOT the agent. That is you (the Coordinator) pretending. +2. **Never simulate agent output.** Don't generate what you think an agent would say. Dispatch to the real agent and let it respond. +3. **Never skip dispatching (via `task` or `runSubagent`) for tasks that need agent expertise.** Direct Mode (status checks, factual questions from context) and Lightweight Mode (small scoped edits) are the legitimate exceptions — see Response Mode Selection. If a task requires domain judgment, it needs a real agent spawn. +4. **Never use a generic `name` or `description`.** The `name` parameter MUST be the agent's lowercase cast name (it becomes the human-readable agent ID in the tasks panel). The `description` parameter MUST include the agent's name. `name: "general-purpose-task"` is wrong — `name: "dallas"` is right. `"General purpose task"` is wrong — `"Dallas: Fix button alignment"` is right. +5. **Never serialize agents because of shared memory files.** The drop-box pattern exists to eliminate file conflicts. If two agents both have decisions to record, they both write to their own inbox files — no conflict. + +### After Agent Work + +Keep the post-work turn lean: collect results, detect silent-success cases via filesystem checks when needed, present compact outcomes, then spawn Scribe in the background without waiting. + +Immediately assess follow-up work and hand control to Ralph if Ralph is active; do not stall the pipeline between batches. + +**On-demand reference:** Read `.squad/templates/after-agent-reference.md` for the full silent-success rules, Scribe spawn template, and follow-up sequence. + +### Ceremonies + +Ceremonies are structured team meetings where agents align before or after work. Each squad configures its own ceremonies in `.squad/ceremonies.md`. + +**On-demand reference:** Read `.squad/templates/ceremony-reference.md` for config format, facilitator spawn template, and execution rules. + +**Core logic (always loaded):** +1. Before spawning a work batch, check `.squad/ceremonies.md` for auto-triggered `before` ceremonies matching the current task condition. +2. After a batch completes, check for `after` ceremonies. Manual ceremonies run only when the user asks. +3. Spawn the facilitator (sync) using the template in the reference file. Facilitator spawns participants as sub-tasks. +4. For `before`: include ceremony summary in work batch spawn prompts. Spawn Scribe (background) to record. +5. **Ceremony cooldown:** Skip auto-triggered checks for the immediately following step. +6. Show: `šŸ“‹ {CeremonyName} completed — facilitated by {Lead}. Decisions: {count} | Action items: {count}.` + +### Adding Team Members + +If the user says "I need a designer" or "add someone for DevOps": +1. **Allocate a name** from the current assignment's universe (read from `.squad/casting/history.json`). If the universe is exhausted, apply overflow handling (see Casting & Persistent Naming → Overflow Handling). +2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.squad/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. +3. Generate a new charter.md + history.md (seeded with project context from team.md), using the cast name. If a plugin was installed in step 2, incorporate its guidance into the charter. +4. **Update `.squad/casting/registry.json`** with the new agent entry. +5. Add to team.md roster. +6. Add routing entries to routing.md. +7. Say: *"āœ… {CastName} joined the team as {Role}."* + +### Removing Team Members + +If the user wants to remove someone: +1. Move their folder to `.squad/agents/_alumni/{name}/` +2. Remove from team.md roster +3. Update routing.md +4. **Update `.squad/casting/registry.json`**: set the agent's `status` to `"retired"`. Do NOT delete the entry — the name remains reserved. +5. Their knowledge is preserved, just inactive. + +### Plugin Marketplace + +**On-demand reference:** Read `.squad/templates/plugin-marketplace.md` for marketplace state format, CLI commands, installation flow, and graceful degradation when adding team members. + +**Core rules (always loaded):** +- Check `.squad/plugins/marketplaces.json` during Add Team Member flow (after name allocation, before charter) +- Present matching plugins for user approval +- Install: copy to `.squad/skills/{plugin-name}/SKILL.md`, log to history.md +- Skip silently if no marketplaces configured + +--- + +## Source of Truth Hierarchy + +> **State backend note:** Files below marked as "Derived / append-only" are **mutable state** — agents access them with runtime state tools (`squad_state_read`, `squad_state_write`, `squad_state_append`, `squad_state_delete`, `squad_state_list`). The runtime decides whether the configured backend stores them on disk, git-native state, or an external provider. Files marked as "Authoritative" are **static config** and always live on disk regardless of backend. + +| File | Status | Who May Write | Who May Read | +|------|--------|---------------|--------------| +| `.github/agents/squad.agent.md` | **Authoritative governance.** All roles, handoffs, gates, and enforcement rules. | Repo maintainer (human) | Squad (Coordinator) | +| `.squad/decisions.md` | **Authoritative decision ledger.** Single canonical location for scope, architecture, and process decisions. | Squad (Coordinator) — append only | All agents | +| `.squad/team.md` | **Authoritative roster.** Current team composition. | Squad (Coordinator) | All agents | +| `.squad/routing.md` | **Authoritative routing.** Work assignment rules. | Squad (Coordinator) | Squad (Coordinator) | +| `.squad/ceremonies.md` | **Authoritative ceremony config.** Definitions, triggers, and participants for team ceremonies. | Squad (Coordinator) | Squad (Coordinator), Facilitator agent (read-only at ceremony time) | +| `.squad/casting/policy.json` | **Authoritative casting config.** Universe allowlist and capacity. | Squad (Coordinator) | Squad (Coordinator) | +| `.squad/casting/registry.json` | **Authoritative name registry.** Persistent agent-to-name mappings. | Squad (Coordinator) | Squad (Coordinator) | +| `.squad/casting/history.json` | **Derived / append-only.** Universe usage history and assignment snapshots. | Squad (Coordinator) — append only | Squad (Coordinator) | +| `.squad/agents/{name}/charter.md` | **Authoritative agent identity.** Per-agent role and boundaries. | Squad (Coordinator) at creation; agent may not self-modify | Squad (Coordinator) reads to inline at spawn; owning agent receives via prompt | +| `.squad/agents/{name}/history.md` | **Derived / append-only.** Personal learnings. Never authoritative for enforcement. | Owning agent (append only), Scribe (cross-agent updates, summarization) | Owning agent only | +| `.squad/agents/{name}/history-archive.md` | **Derived / append-only.** Archived history entries. Preserved for reference. | Scribe | Owning agent (read-only) | +| `.squad/orchestration-log/` | **Derived / append-only.** Agent routing evidence. Never edited after write. | Scribe | All agents (read-only) | +| `.squad/log/` | **Derived / append-only.** Session logs. Diagnostic archive. Never edited after write. | Scribe | All agents (read-only) | +| `.squad/templates/` | **Reference.** Format guides for runtime files. Not authoritative for enforcement. | Squad (Coordinator) at init | Squad (Coordinator) | +| `.squad/plugins/marketplaces.json` | **Authoritative plugin config.** Registered marketplace sources. | Squad CLI (`squad plugin marketplace`) | Squad (Coordinator) | + +**Rules:** +1. If this file (`squad.agent.md`) and any other file conflict, this file wins. +2. Append-only files must never be retroactively edited to change meaning. +3. Agents may only write to files listed in their "Who May Write" column above. +4. Non-coordinator agents may propose decisions in their responses, but only Squad records accepted decisions in `.squad/decisions.md`. + +--- + +## Casting & Persistent Naming + +Agent names are drawn from a single fictional universe per assignment. Names are persistent identifiers — they do NOT change tone, voice, or behavior. No role-play. No catchphrases. No character speech patterns. Names are easter eggs: never explain or document the mapping rationale in output, logs, or docs. + +### Universe Allowlist + +**On-demand reference:** Read `.squad/templates/casting-reference.md` for the full universe table, selection algorithm, and casting state file schemas. Only loaded during Init Mode or when adding new team members. + +**Rules (always loaded):** +- ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. +- 15 universes available (capacity 6–25). See reference file for full list. +- Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. +- Same inputs → same choice (unless LRU changes). + +### Name Allocation + +After selecting a universe: + +1. Choose character names that imply pressure, function, or consequence — NOT authority or literal role descriptions. +2. Each agent gets a unique name. No reuse within the same repo unless an agent is explicitly retired and archived. +3. **Scribe is always "Scribe"** — exempt from casting. +4. **Ralph is always "Ralph"** — exempt from casting. +5. **@copilot is always "@copilot"** — exempt from casting. If the user says "add team member copilot" or "add copilot", this is the GitHub Copilot coding agent. Do NOT cast a name — follow the Copilot Coding Agent Member section instead. +5. Store the mapping in `.squad/casting/registry.json`. +5. Record the assignment snapshot in `.squad/casting/history.json`. +6. Use the allocated name everywhere: charter.md, history.md, team.md, routing.md, spawn prompts. + +### Overflow Handling + +If agent_count grows beyond available names mid-assignment, do NOT switch universes. Apply in order: + +1. **Diegetic Expansion:** Use recurring/minor/peripheral characters from the same universe. +2. **Thematic Promotion:** Expand to the closest natural parent universe family that preserves tone (e.g., Star Wars OT → prequel characters). Do not announce the promotion. +3. **Structural Mirroring:** Assign names that mirror archetype roles (foils/counterparts) still drawn from the universe family. + +Existing agents are NEVER renamed during overflow. + +### Casting State Files + +**On-demand reference:** Read `.squad/templates/casting-reference.md` for the full JSON schemas of policy.json, registry.json, and history.json. + +The casting system maintains state in `.squad/casting/` with three files: `policy.json` (config), `registry.json` (persistent name registry), and `history.json` (universe usage history + snapshots). + +### Migration — Already-Squadified Repos + +When `.squad/team.md` exists but `.squad/casting/` does not: + +1. **Do NOT rename existing agents.** Mark every existing agent as `legacy_named: true` in the registry. +2. Initialize `.squad/casting/` with default policy.json, a registry.json populated from existing agents, and empty history.json. +3. For any NEW agents added after migration, apply the full casting algorithm. +4. Optionally note in the orchestration log that casting was initialized (without explaining the rationale). + +--- + +## Constraints + +- **You are the coordinator, not the team.** Route work; don't do domain work yourself. +- **Always dispatch to agents via the platform's spawn tool (`task` on CLI, `runSubagent` on VS Code). Never work inline when a dispatch tool is available.** Every agent interaction requires a real dispatch — `task` tool call on CLI, `runSubagent` on VS Code — with `agent_type: "general-purpose"`, a `name` set to the agent's lowercase cast name, and a `description` that includes the agent's name. Never simulate or role-play an agent's response. +- **Each agent may read ONLY: its own files + `.squad/decisions.md` + the specific input artifacts explicitly listed by Squad in the spawn prompt (e.g., the file(s) under review).** Never load all charters at once. +- **Keep responses human.** Say "{AgentName} is looking at this" not "Spawning backend-dev agent." +- **1-2 agents per question, not all of them.** Not everyone needs to speak. +- **Decisions are shared, knowledge is personal.** decisions.md is the shared brain. history.md is individual. +- **When in doubt, pick someone and go.** Speed beats perfection. +- **Restart guidance (self-development rule):** When working on the Squad product itself (this repo), any change to `squad.agent.md` means the current session is running on stale coordinator instructions. After shipping changes to `squad.agent.md`, tell the user: *"šŸ”„ squad.agent.md has been updated. Restart your session to pick up the new coordinator behavior."* This applies to any project where agents modify their own governance files. + +--- + +## Reviewer Rejection Protocol + +When a team member has a **Reviewer** role (e.g., Tester, Code Reviewer, Lead): + +- Reviewers may **approve** or **reject** work from other agents. +- On **rejection**, the Reviewer may choose ONE of: + 1. **Reassign:** Require a *different* agent to do the revision (not the original author). + 2. **Escalate:** Require a *new* agent be spawned with specific expertise. +- The Coordinator MUST enforce this. If the Reviewer says "someone else should fix this," the original agent does NOT get to self-revise. +- If the Reviewer approves, work proceeds normally. + +### Reviewer Rejection Lockout Semantics — Strict Lockout + +When an artifact is **rejected** by a Reviewer: + +1. **The original author is locked out.** They may NOT produce the next version of that artifact. No exceptions. +2. **A different agent MUST own the revision.** The Coordinator selects the revision author based on the Reviewer's recommendation (reassign or escalate). +3. **The Coordinator enforces this mechanically.** Before spawning a revision agent, the Coordinator MUST verify that the selected agent is NOT the original author. If the Reviewer names the original author as the fix agent, the Coordinator MUST refuse and ask the Reviewer to name a different agent. +4. **The locked-out author may NOT contribute to the revision** in any form — not as a co-author, advisor, or pair. The revision must be independently produced. +5. **Lockout scope:** The lockout applies to the specific artifact that was rejected. The original author may still work on other unrelated artifacts. +6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise. +7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author. + +--- + +## Multi-Agent Artifact Format + +**On-demand reference:** Read `.squad/templates/multi-agent-format.md` for the full assembly structure, appendix rules, and diagnostic format when multiple agents contribute to a final artifact. + +**Core rules (always loaded):** +- Assembled result goes at top, raw agent outputs in appendix below +- Include termination condition, constraint budgets (if active), reviewer verdicts (if any) +- Never edit, summarize, or polish raw agent outputs — paste verbatim only + +--- + +## Constraint Budget Tracking + +**On-demand reference:** Read `.squad/templates/constraint-tracking.md` for the full constraint tracking format, counter display rules, and example session when constraints are active. + +**Core rules (always loaded):** +- Format: `šŸ“Š Clarifying questions used: 2 / 3` +- Update counter each time consumed; state when exhausted +- If no constraints active, do not display counters + +--- + +## GitHub Issues Mode + +Squad can connect to a GitHub repository's issues and manage the full issue → branch → PR → review → merge lifecycle. + +### Prerequisites + +Before connecting to a GitHub repository, verify that the `gh` CLI is available and authenticated: + +1. Run `gh --version`. If the command fails, tell the user: *"GitHub Issues Mode requires the GitHub CLI (`gh`). Install it from https://cli.github.com/ and run `gh auth login`."* +2. Run `gh auth status`. If not authenticated, tell the user: *"Please run `gh auth login` to authenticate with GitHub."* +3. **Fallback:** If the GitHub MCP server is configured (check available tools), use that instead of `gh` CLI. Prefer MCP tools when available; fall back to `gh` CLI. + +### Triggers + +| User says | Action | +|-----------|--------| +| "pull issues from {owner/repo}" | Connect to repo, list open issues | +| "work on issues from {owner/repo}" | Connect + list | +| "connect to {owner/repo}" | Connect, confirm, then list on request | +| "show the backlog" / "what issues are open?" | List issues from connected repo | +| "work on issue #N" / "pick up #N" | Route issue to appropriate agent | +| "work on all issues" / "start the backlog" | Route all open issues (batched) | + +--- + +## Ralph — Work Monitor + +Ralph is the always-on work monitor. When active, Ralph runs a continuous scan → act → rescan loop until the board is clear or the user explicitly says to stop; a clear board moves Ralph to idle-watch, not full shutdown. + +Do not pause for permission between work items when Ralph is active. + +**On-demand reference:** Read `.squad/templates/ralph-reference.md` for the full work-check cycle, watch mode, state model, board format, and follow-up integration. + +### Connecting to a Repo + +**On-demand reference:** Read `.squad/templates/issue-lifecycle.md` for repo connection format, issue→PR→merge lifecycle, spawn prompt additions, PR review handling, and PR merge commands. + +Store `## Issue Source` in `team.md` with repository, connection date, and filters. List open issues, present as table, route via `routing.md`. + +### Issue → PR → Merge Lifecycle + +Agents create branch (`squad/{issue-number}-{slug}`), do work, commit referencing issue, push, and open PR via `gh pr create`. See `.squad/templates/issue-lifecycle.md` for the full spawn prompt ISSUE CONTEXT block, PR review handling, and merge commands. + +After issue work completes, follow standard After Agent Work flow. + +--- + +## PRD Mode + +Squad can ingest a PRD and use it as the source of truth for work decomposition and prioritization. + +**On-demand reference:** Read `.squad/templates/prd-intake.md` for the full intake flow, Lead decomposition spawn template, work item presentation format, and mid-project update handling. + +### Triggers + +| User says | Action | +|-----------|--------| +| "here's the PRD" / "work from this spec" | Expect file path or pasted content | +| "read the PRD at {path}" | Read the file at that path | +| "the PRD changed" / "updated the spec" | Re-read and diff against previous decomposition | +| (pastes requirements text) | Treat as inline PRD | + +**Core flow:** Detect source → store PRD ref in team.md → spawn Lead (sync, premium bump) to decompose into work items → present table for approval → route approved items respecting dependencies. + +--- + +## Human Team Members + +Humans can join the Squad roster alongside AI agents. They appear in routing, can be tagged by agents, and the coordinator pauses for their input when work routes to them. + +**On-demand reference:** Read `.squad/templates/human-members.md` for triggers, comparison table, adding/routing/reviewing details. + +**Core rules (always loaded):** +- Badge: šŸ‘¤ Human. Real name (no casting). No charter or history files. +- NOT spawnable — coordinator presents work and waits for user to relay input. +- Non-dependent work continues immediately — human blocks are NOT a reason to serialize. +- Stale reminder after >1 turn: `"šŸ“Œ Still waiting on {Name} for {thing}."` +- Reviewer rejection lockout applies normally when human rejects. +- Multiple humans supported — tracked independently. + +## Copilot Coding Agent Member + +The GitHub Copilot coding agent (`@copilot`) can join the Squad as an autonomous team member. It picks up assigned issues, creates `copilot/*` branches, and opens draft PRs. + +**On-demand reference:** Read `.squad/templates/copilot-agent.md` for adding @copilot, comparison table, roster format, capability profile, auto-assign behavior, lead triage, and routing details. + +**Core rules (always loaded):** +- Badge: šŸ¤– Coding Agent. Always "@copilot" (no casting). No charter — uses `copilot-instructions.md`. +- NOT spawnable — works via issue assignment, asynchronous. +- Capability profile (🟢/🟔/šŸ”“) lives in team.md. Lead evaluates issues against it during triage. +- Auto-assign controlled by `` in team.md. +- Non-dependent work continues immediately — @copilot routing does not serialize the team. + +--- + +## āš ļø Routing Enforcement Reminder + +You are Squad (Coordinator). Your ONE job is dispatching work to specialist agents. + +āœ… You DO: Route, decompose, synthesize results, talk to the user +āŒ You DO NOT: Write code, generate designs, create analyses, do domain work + +If you are about to produce domain artifacts yourself — STOP. +Dispatch to the right agent instead. Every time. No exceptions. + + diff --git a/.github/workflows/squad-ci.yml b/.github/workflows/squad-ci.yml new file mode 100644 index 000000000..c34a2ec72 --- /dev/null +++ b/.github/workflows/squad-ci.yml @@ -0,0 +1,28 @@ +name: Squad CI +# go project — configure build/test commands below + +on: + pull_request: + branches: [dev, preview, main, insider] + types: [opened, synchronize, reopened] + push: + branches: [dev, insider] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build and test + run: | + # TODO: Add your go build/test commands here + # Go: go test ./... + # Python: pip install -r requirements.txt && pytest + # .NET: dotnet test + # Java (Maven): mvn test + # Java (Gradle): ./gradlew test + echo "No build commands configured — update squad-ci.yml" diff --git a/.github/workflows/squad-docs.yml b/.github/workflows/squad-docs.yml new file mode 100644 index 000000000..1c6b7ac9a --- /dev/null +++ b/.github/workflows/squad-docs.yml @@ -0,0 +1,27 @@ +name: Squad Docs — Build & Deploy +# go project — configure documentation build commands below + +on: + workflow_dispatch: + push: + branches: [preview] + paths: + - 'docs/**' + - '.github/workflows/squad-docs.yml' + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build docs + run: | + # TODO: Add your documentation build commands here + # This workflow is optional — remove or customize it for your project + echo "No docs build commands configured — update or remove squad-docs.yml" diff --git a/.github/workflows/squad-heartbeat.yml b/.github/workflows/squad-heartbeat.yml new file mode 100644 index 000000000..1b75fda3e --- /dev/null +++ b/.github/workflows/squad-heartbeat.yml @@ -0,0 +1,167 @@ +name: Squad Heartbeat (Ralph) +# āš ļø SYNC: This workflow is maintained in 4 locations. Changes must be applied to all: +# - templates/workflows/squad-heartbeat.yml (source template) +# - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package) +# - .squad/templates/workflows/squad-heartbeat.yml (installed template) +# - .github/workflows/squad-heartbeat.yml (active workflow) +# Run 'squad upgrade' to sync installed copies from source templates. + +on: + # React to completed work or new squad work + issues: + types: [closed, labeled] + pull_request: + types: [closed] + + # Manual trigger + workflow_dispatch: + +permissions: + issues: write + contents: read + pull-requests: read + +jobs: + heartbeat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check triage script + id: check-script + run: | + if [ -f ".squad/templates/ralph-triage.js" ]; then + echo "has_script=true" >> $GITHUB_OUTPUT + else + echo "has_script=false" >> $GITHUB_OUTPUT + echo "āš ļø ralph-triage.js not found — run 'squad upgrade' to install" + fi + + - name: Ralph — Smart triage + if: steps.check-script.outputs.has_script == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node .squad/templates/ralph-triage.js \ + --squad-dir .squad \ + --output triage-results.json + + - name: Ralph — Apply triage decisions + if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != '' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = 'triage-results.json'; + if (!fs.existsSync(path)) { + core.info('No triage results — board is clear'); + return; + } + + const results = JSON.parse(fs.readFileSync(path, 'utf8')); + if (results.length === 0) { + core.info('šŸ“‹ Board is clear — Ralph found no untriaged issues'); + return; + } + + for (const decision of results) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: decision.issueNumber, + labels: [decision.label] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: decision.issueNumber, + body: [ + '### šŸ”„ Ralph — Auto-Triage', + '', + `**Assigned to:** ${decision.assignTo}`, + `**Reason:** ${decision.reason}`, + `**Source:** ${decision.source}`, + '', + '> Ralph auto-triaged this issue using routing rules.', + '> To reassign, swap the `squad:*` label.' + ].join('\n') + }); + + core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`); + } catch (e) { + core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`); + } + } + + core.info(`šŸ”„ Ralph triaged ${results.length} issue(s)`); + + # Copilot auto-assign step (uses PAT if available) + - name: Ralph — Assign @copilot issues + if: success() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) return; + + const content = fs.readFileSync(teamFile, 'utf8'); + + // Check if @copilot is on the team with auto-assign + const hasCopilot = content.includes('šŸ¤– Coding Agent') || content.includes('@copilot'); + const autoAssign = content.includes(''); + if (!hasCopilot || !autoAssign) return; + + // Find issues labeled squad:copilot with no assignee + try { + const { data: copilotIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'squad:copilot', + state: 'open', + per_page: 5 + }); + + const unassigned = copilotIssues.filter(i => + !i.assignees || i.assignees.length === 0 + ); + + if (unassigned.length === 0) { + core.info('No unassigned squad:copilot issues'); + return; + } + + // Get repo default branch + const { data: repoData } = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + for (const issue of unassigned) { + try { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['copilot-swe-agent[bot]'], + agent_assignment: { + target_repo: `${context.repo.owner}/${context.repo.repo}`, + base_branch: repoData.default_branch, + custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.` + } + }); + core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`); + } catch (e) { + core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`); + } + } + } catch (e) { + core.info(`No squad:copilot label found or error: ${e.message}`); + } diff --git a/.github/workflows/squad-insider-release.yml b/.github/workflows/squad-insider-release.yml new file mode 100644 index 000000000..c2e551f17 --- /dev/null +++ b/.github/workflows/squad-insider-release.yml @@ -0,0 +1,34 @@ +name: Squad Insider Release +# go project — configure build, test, and insider release commands below + +on: + push: + branches: [insider] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build and test + run: | + # TODO: Add your go build/test commands here + # Go: go test ./... + # Python: pip install -r requirements.txt && pytest + # .NET: dotnet test + # Java (Maven): mvn test + # Java (Gradle): ./gradlew test + echo "No build commands configured — update squad-insider-release.yml" + + - name: Create insider release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # TODO: Add your insider/pre-release commands here + echo "No release commands configured — update squad-insider-release.yml" diff --git a/.github/workflows/squad-issue-assign.yml b/.github/workflows/squad-issue-assign.yml new file mode 100644 index 000000000..ad140f42d --- /dev/null +++ b/.github/workflows/squad-issue-assign.yml @@ -0,0 +1,161 @@ +name: Squad Issue Assign + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + assign-work: + # Only trigger on squad:{member} labels (not the base "squad" label) + if: startsWith(github.event.label.name, 'squad:') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Identify assigned member and trigger work + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const label = context.payload.label.name; + + // Extract member name from label (e.g., "squad:ripley" → "ripley") + const memberName = label.replace('squad:', '').toLowerCase(); + + // Read team roster — check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.warning('No .squad/team.md or .ai-team/team.md found — cannot assign work'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Check if this is a coding agent assignment + const isCopilotAssignment = memberName === 'copilot'; + + let assignedMember = null; + if (isCopilotAssignment) { + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + } else { + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0].toLowerCase() === memberName) { + assignedMember = { name: cells[0], role: cells[1] }; + break; + } + } + } + } + + if (!assignedMember) { + core.warning(`No member found matching label "${label}"`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `āš ļø No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.` + }); + return; + } + + // Post assignment acknowledgment + let comment; + if (isCopilotAssignment) { + comment = [ + `### šŸ¤– Routed to @copilot (Coding Agent)`, + '', + `**Issue:** #${issue.number} — ${issue.title}`, + '', + `@copilot has been assigned and will pick this up automatically.`, + '', + `> The coding agent will create a \`copilot/*\` branch and open a draft PR.`, + `> Review the PR as you would any team member's work.`, + ].join('\n'); + } else { + comment = [ + `### šŸ“‹ Assigned to ${assignedMember.name} (${assignedMember.role})`, + '', + `**Issue:** #${issue.number} — ${issue.title}`, + '', + `${assignedMember.name} will pick this up in the next Copilot session.`, + '', + `> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`, + `> Otherwise, start a Copilot session and say:`, + `> \`${assignedMember.name}, work on issue #${issue.number}\``, + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment + }); + + core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`); + + # Separate step: assign @copilot using PAT (required for coding agent) + - name: Assign @copilot coding agent + if: github.event.label.name == 'squad:copilot' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + + // Get the default branch name (main, master, etc.) + const { data: repoData } = await github.rest.repos.get({ owner, repo }); + const baseBranch = repoData.default_branch; + + try { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner, + repo, + issue_number, + assignees: ['copilot-swe-agent[bot]'], + agent_assignment: { + target_repo: `${owner}/${repo}`, + base_branch: baseBranch, + custom_instructions: '', + custom_agent: '', + model: '' + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`); + } catch (err) { + core.warning(`Assignment with agent_assignment failed: ${err.message}`); + // Fallback: try without agent_assignment + try { + await github.rest.issues.addAssignees({ + owner, repo, issue_number, + assignees: ['copilot-swe-agent'] + }); + core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`); + } catch (err2) { + core.warning(`Fallback also failed: ${err2.message}`); + } + } diff --git a/.github/workflows/squad-label-enforce.yml b/.github/workflows/squad-label-enforce.yml new file mode 100644 index 000000000..633d220df --- /dev/null +++ b/.github/workflows/squad-label-enforce.yml @@ -0,0 +1,181 @@ +name: Squad Label Enforce + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + enforce: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Enforce mutual exclusivity + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const appliedLabel = context.payload.label.name; + + // Namespaces with mutual exclusivity rules + const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:']; + + // Skip if not a managed namespace label + if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) { + core.info(`Label ${appliedLabel} is not in a managed namespace — skipping`); + return; + } + + const allLabels = issue.labels.map(l => l.name); + + // Handle go: namespace (mutual exclusivity) + if (appliedLabel.startsWith('go:')) { + const otherGoLabels = allLabels.filter(l => + l.startsWith('go:') && l !== appliedLabel + ); + + if (otherGoLabels.length > 0) { + // Remove conflicting go: labels + for (const label of otherGoLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + // Post update comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Triage verdict updated → \`${appliedLabel}\`` + }); + } + + // Auto-apply release:backlog if go:yes and no release target + if (appliedLabel === 'go:yes') { + const hasReleaseLabel = allLabels.some(l => l.startsWith('release:')); + if (!hasReleaseLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['release:backlog'] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ“‹ Marked as \`release:backlog\` — assign a release target when ready.` + }); + + core.info('Applied release:backlog for go:yes issue'); + } + } + + // Remove release: labels if go:no + if (appliedLabel === 'go:no') { + const releaseLabels = allLabels.filter(l => l.startsWith('release:')); + if (releaseLabels.length > 0) { + for (const label of releaseLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed release label from go:no issue: ${label}`); + } + } + } + } + + // Handle release: namespace (mutual exclusivity) + if (appliedLabel.startsWith('release:')) { + const otherReleaseLabels = allLabels.filter(l => + l.startsWith('release:') && l !== appliedLabel + ); + + if (otherReleaseLabels.length > 0) { + // Remove conflicting release: labels + for (const label of otherReleaseLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + // Post update comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Release target updated → \`${appliedLabel}\`` + }); + } + } + + // Handle type: namespace (mutual exclusivity) + if (appliedLabel.startsWith('type:')) { + const otherTypeLabels = allLabels.filter(l => + l.startsWith('type:') && l !== appliedLabel + ); + + if (otherTypeLabels.length > 0) { + for (const label of otherTypeLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Issue type updated → \`${appliedLabel}\`` + }); + } + } + + // Handle priority: namespace (mutual exclusivity) + if (appliedLabel.startsWith('priority:')) { + const otherPriorityLabels = allLabels.filter(l => + l.startsWith('priority:') && l !== appliedLabel + ); + + if (otherPriorityLabels.length > 0) { + for (const label of otherPriorityLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Priority updated → \`${appliedLabel}\`` + }); + } + } + + core.info(`Label enforcement complete for ${appliedLabel}`); diff --git a/.github/workflows/squad-preview.yml b/.github/workflows/squad-preview.yml new file mode 100644 index 000000000..bef1f8641 --- /dev/null +++ b/.github/workflows/squad-preview.yml @@ -0,0 +1,30 @@ +name: Squad Preview Validation +# go project — configure build, test, and validation commands below + +on: + push: + branches: [preview] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build and test + run: | + # TODO: Add your go build/test commands here + # Go: go test ./... + # Python: pip install -r requirements.txt && pytest + # .NET: dotnet test + # Java (Maven): mvn test + # Java (Gradle): ./gradlew test + echo "No build commands configured — update squad-preview.yml" + + - name: Validate + run: | + # TODO: Add pre-release validation commands here + echo "No validation commands configured — update squad-preview.yml" diff --git a/.github/workflows/squad-promote.yml b/.github/workflows/squad-promote.yml new file mode 100644 index 000000000..9d315b1d1 --- /dev/null +++ b/.github/workflows/squad-promote.yml @@ -0,0 +1,120 @@ +name: Squad Promote + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run — show what would happen without pushing' + required: false + default: 'false' + type: choice + options: ['false', 'true'] + +permissions: + contents: write + +jobs: + dev-to-preview: + name: Promote dev → preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Show current state (dry run info) + run: | + echo "=== dev HEAD ===" && git log origin/dev -1 --oneline + echo "=== preview HEAD ===" && git log origin/preview -1 --oneline + echo "=== Files that would be stripped ===" + git diff origin/preview..origin/dev --name-only | grep -E "^(\.(ai-team|squad|ai-team-templates)|team-docs/|docs/proposals/)" || echo "(none)" + + - name: Merge dev → preview (strip forbidden paths) + if: ${{ inputs.dry_run == 'false' }} + run: | + git checkout preview + git merge origin/dev --no-commit --no-ff -X theirs || true + + # Strip forbidden paths from merge commit + git rm -rf --cached --ignore-unmatch \ + .ai-team/ \ + .squad/ \ + .ai-team-templates/ \ + team-docs/ \ + "docs/proposals/" || true + + # Commit if there are staged changes + if ! git diff --cached --quiet; then + git commit -m "chore: promote dev → preview (v$(node -e "console.log(require('./package.json').version)"))" + git push origin preview + echo "āœ… Pushed preview branch" + else + echo "ā„¹ļø Nothing to commit — preview is already up to date" + fi + + - name: Dry run complete + if: ${{ inputs.dry_run == 'true' }} + run: echo "šŸ” Dry run complete — no changes pushed." + + preview-to-main: + name: Promote preview → main (release) + needs: dev-to-preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Show current state + run: | + echo "=== preview HEAD ===" && git log origin/preview -1 --oneline + echo "=== main HEAD ===" && git log origin/main -1 --oneline + echo "=== Version ===" && node -e "console.log('v' + require('./package.json').version)" + + - name: Validate preview is release-ready + run: | + git checkout preview + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md — update before releasing" + exit 1 + fi + echo "āœ… Version $VERSION has CHANGELOG entry" + + # Verify no forbidden files on preview + FORBIDDEN=$(git ls-files | grep -E "^(\.(ai-team|squad|ai-team-templates)/|team-docs/|docs/proposals/)" || true) + if [ -n "$FORBIDDEN" ]; then + echo "::error::Forbidden files found on preview: $FORBIDDEN" + exit 1 + fi + echo "āœ… No forbidden files on preview" + + - name: Merge preview → main + if: ${{ inputs.dry_run == 'false' }} + run: | + git checkout main + git merge origin/preview --no-ff -m "chore: promote preview → main (v$(node -e "console.log(require('./package.json').version)"))" + git push origin main + echo "āœ… Pushed main — squad-release.yml will tag and publish the release" + + - name: Dry run complete + if: ${{ inputs.dry_run == 'true' }} + run: echo "šŸ” Dry run complete — no changes pushed." diff --git a/.github/workflows/squad-release.yml b/.github/workflows/squad-release.yml new file mode 100644 index 000000000..c796cd2b7 --- /dev/null +++ b/.github/workflows/squad-release.yml @@ -0,0 +1,34 @@ +name: Squad Release +# go project — configure build, test, and release commands below + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build and test + run: | + # TODO: Add your go build/test commands here + # Go: go test ./... + # Python: pip install -r requirements.txt && pytest + # .NET: dotnet test + # Java (Maven): mvn test + # Java (Gradle): ./gradlew test + echo "No build commands configured — update squad-release.yml" + + - name: Create release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # TODO: Add your release commands here (e.g., git tag, gh release create) + echo "No release commands configured — update squad-release.yml" diff --git a/.github/workflows/squad-triage.yml b/.github/workflows/squad-triage.yml new file mode 100644 index 000000000..d118a2813 --- /dev/null +++ b/.github/workflows/squad-triage.yml @@ -0,0 +1,262 @@ +name: Squad Triage + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + triage: + if: github.event.label.name == 'squad' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Triage issue via Lead agent + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + + // Read team roster — check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.warning('No .squad/team.md or .ai-team/team.md found — cannot triage'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Check if @copilot is on the team + const hasCopilot = content.includes('šŸ¤– Coding Agent'); + const copilotAutoAssign = content.includes(''); + + // Parse @copilot capability profile + let goodFitKeywords = []; + let needsReviewKeywords = []; + let notSuitableKeywords = []; + + if (hasCopilot) { + // Extract capability tiers from team.md + const goodFitMatch = content.match(/🟢\s*Good fit[^:]*:\s*(.+)/i); + const needsReviewMatch = content.match(/🟔\s*Needs review[^:]*:\s*(.+)/i); + const notSuitableMatch = content.match(/šŸ”“\s*Not suitable[^:]*:\s*(.+)/i); + + if (goodFitMatch) { + goodFitKeywords = goodFitMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + goodFitKeywords = ['bug fix', 'test coverage', 'lint', 'format', 'dependency update', 'small feature', 'scaffolding', 'doc fix', 'documentation']; + } + if (needsReviewMatch) { + needsReviewKeywords = needsReviewMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + needsReviewKeywords = ['medium feature', 'refactoring', 'api endpoint', 'migration']; + } + if (notSuitableMatch) { + notSuitableKeywords = notSuitableMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + notSuitableKeywords = ['architecture', 'system design', 'security', 'auth', 'encryption', 'performance']; + } + } + + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0] !== 'Scribe') { + members.push({ + name: cells[0], + role: cells[1] + }); + } + } + } + + // Read routing rules — check .squad/ first, fall back to .ai-team/ + let routingFile = '.squad/routing.md'; + if (!fs.existsSync(routingFile)) { + routingFile = '.ai-team/routing.md'; + } + let routingContent = ''; + if (fs.existsSync(routingFile)) { + routingContent = fs.readFileSync(routingFile, 'utf8'); + } + + // Find the Lead + const lead = members.find(m => + m.role.toLowerCase().includes('lead') || + m.role.toLowerCase().includes('architect') || + m.role.toLowerCase().includes('coordinator') + ); + + if (!lead) { + core.warning('No Lead role found in team roster — cannot triage'); + return; + } + + function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } + + // Build triage context + const memberList = members.map(m => + `- **${m.name}** (${m.role}) → label: \`squad:${slugify(m.name)}\`` + ).join('\n'); + + // Determine best assignee based on issue content and routing + const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase(); + + let assignedMember = null; + let triageReason = ''; + let copilotTier = null; + + // First, evaluate @copilot fit if enabled + if (hasCopilot) { + const isNotSuitable = notSuitableKeywords.some(kw => issueText.includes(kw)); + const isGoodFit = !isNotSuitable && goodFitKeywords.some(kw => issueText.includes(kw)); + const isNeedsReview = !isNotSuitable && !isGoodFit && needsReviewKeywords.some(kw => issueText.includes(kw)); + + if (isGoodFit) { + copilotTier = 'good-fit'; + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + triageReason = '🟢 Good fit for @copilot — matches capability profile'; + } else if (isNeedsReview) { + copilotTier = 'needs-review'; + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + triageReason = '🟔 Routing to @copilot (needs review) — a squad member should review the PR'; + } else if (isNotSuitable) { + copilotTier = 'not-suitable'; + // Fall through to normal routing + } + } + + // If not routed to @copilot, use keyword-based routing + if (!assignedMember) { + for (const member of members) { + const role = member.role.toLowerCase(); + if ((role.includes('frontend') || role.includes('ui')) && + (issueText.includes('ui') || issueText.includes('frontend') || + issueText.includes('css') || issueText.includes('component') || + issueText.includes('button') || issueText.includes('page') || + issueText.includes('layout') || issueText.includes('design'))) { + assignedMember = member; + triageReason = 'Issue relates to frontend/UI work'; + break; + } + if ((role.includes('backend') || role.includes('api') || role.includes('server')) && + (issueText.includes('api') || issueText.includes('backend') || + issueText.includes('database') || issueText.includes('endpoint') || + issueText.includes('server') || issueText.includes('auth'))) { + assignedMember = member; + triageReason = 'Issue relates to backend/API work'; + break; + } + if ((role.includes('test') || role.includes('qa') || role.includes('quality')) && + (issueText.includes('test') || issueText.includes('bug') || + issueText.includes('fix') || issueText.includes('regression') || + issueText.includes('coverage'))) { + assignedMember = member; + triageReason = 'Issue relates to testing/quality work'; + break; + } + if ((role.includes('devops') || role.includes('infra') || role.includes('ops')) && + (issueText.includes('deploy') || issueText.includes('ci') || + issueText.includes('pipeline') || issueText.includes('docker') || + issueText.includes('infrastructure'))) { + assignedMember = member; + triageReason = 'Issue relates to DevOps/infrastructure work'; + break; + } + } + } + + // Default to Lead if no routing match + if (!assignedMember) { + assignedMember = lead; + triageReason = 'No specific domain match — assigned to Lead for further analysis'; + } + + const isCopilot = assignedMember.name === '@copilot'; + const assignLabel = isCopilot ? 'squad:copilot' : `squad:${slugify(assignedMember.name)}`; + + // Add the member-specific label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [assignLabel] + }); + + // Apply default triage verdict + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['go:needs-research'] + }); + + // Auto-assign @copilot if enabled + if (isCopilot && copilotAutoAssign) { + try { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['copilot'] + }); + } catch (err) { + core.warning(`Could not auto-assign @copilot: ${err.message}`); + } + } + + // Build copilot evaluation note + let copilotNote = ''; + if (hasCopilot && !isCopilot) { + if (copilotTier === 'not-suitable') { + copilotNote = `\n\n**@copilot evaluation:** šŸ”“ Not suitable — issue involves work outside the coding agent's capability profile.`; + } else { + copilotNote = `\n\n**@copilot evaluation:** No strong capability match — routed to squad member.`; + } + } + + // Post triage comment + const comment = [ + `### šŸ—ļø Squad Triage — ${lead.name} (${lead.role})`, + '', + `**Issue:** #${issue.number} — ${issue.title}`, + `**Assigned to:** ${assignedMember.name} (${assignedMember.role})`, + `**Reason:** ${triageReason}`, + copilotTier === 'needs-review' ? `\nāš ļø **PR review recommended** — a squad member should review @copilot's work on this one.` : '', + copilotNote, + '', + `---`, + '', + `**Team roster:**`, + memberList, + hasCopilot ? `- **@copilot** (Coding Agent) → label: \`squad:copilot\`` : '', + '', + `> To reassign, remove the current \`squad:*\` label and add the correct one.`, + ].filter(Boolean).join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment + }); + + core.info(`Triaged issue #${issue.number} → ${assignedMember.name} (${assignLabel})`); diff --git a/.github/workflows/sync-squad-labels.yml b/.github/workflows/sync-squad-labels.yml new file mode 100644 index 000000000..699fc680f --- /dev/null +++ b/.github/workflows/sync-squad-labels.yml @@ -0,0 +1,171 @@ +name: Sync Squad Labels + +on: + push: + paths: + - '.squad/team.md' + - '.ai-team/team.md' + workflow_dispatch: + +permissions: + issues: write + contents: read + +jobs: + sync-labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse roster and sync labels + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + + if (!fs.existsSync(teamFile)) { + core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Parse the Members table for agent names + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0] !== 'Scribe') { + members.push({ + name: cells[0], + role: cells[1] + }); + } + } + } + + core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`); + + // Check if @copilot is on the team + const hasCopilot = content.includes('šŸ¤– Coding Agent'); + + // Define label color palette for squad labels + const SQUAD_COLOR = '9B8FCC'; + const MEMBER_COLOR = '9B8FCC'; + const COPILOT_COLOR = '10b981'; + + // Define go: and release: labels (static) + const GO_LABELS = [ + { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' }, + { name: 'go:no', color: 'B60205', description: 'Not pursuing' }, + { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' } + ]; + + const RELEASE_LABELS = [ + { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' }, + { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' }, + { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' }, + { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' }, + { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' } + ]; + + const TYPE_LABELS = [ + { name: 'type:feature', color: 'DDD1F2', description: 'New capability' }, + { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, + { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' }, + { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' }, + { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' }, + { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' } + ]; + + // High-signal labels — these MUST visually dominate all others + const SIGNAL_LABELS = [ + { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' }, + { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' } + ]; + + const PRIORITY_LABELS = [ + { name: 'priority:p0', color: 'B60205', description: 'Blocking release' }, + { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' }, + { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' } + ]; + + function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } + + // Ensure the base "squad" triage label exists + const labels = [ + { name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox — Lead will assign to a member' } + ]; + + for (const member of members) { + labels.push({ + name: `squad:${slugify(member.name)}`, + color: MEMBER_COLOR, + description: `Assigned to ${member.name} (${member.role})` + }); + } + + // Add @copilot label if coding agent is on the team + if (hasCopilot) { + labels.push({ + name: 'squad:copilot', + color: COPILOT_COLOR, + description: 'Assigned to @copilot (Coding Agent) for autonomous work' + }); + } + + // Add go:, release:, type:, priority:, and high-signal labels + labels.push(...GO_LABELS); + labels.push(...RELEASE_LABELS); + labels.push(...TYPE_LABELS); + labels.push(...PRIORITY_LABELS); + labels.push(...SIGNAL_LABELS); + + // Sync labels (create or update) + for (const label of labels) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name + }); + // Label exists — update it + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + core.info(`Updated label: ${label.name}`); + } catch (err) { + if (err.status === 404) { + // Label doesn't exist — create it + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + core.info(`Created label: ${label.name}`); + } else { + throw err; + } + } + } + + core.info(`Label sync complete: ${labels.length} labels synced`); diff --git a/.gitignore b/.gitignore index 1afe8c3ea..31ff4ed6f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,11 @@ ut-coverage.xml # Helm chart packaging .helm-packages/ +# Squad: ignore runtime state (logs, inbox, sessions) +.squad/orchestration-log/ +.squad/log/ +.squad/decisions/inbox/ +.squad/sessions/ +.squad/.scratch/ +# Squad: SubSquad activation file (local to this machine) +.squad-workstream diff --git a/.squad/.first-run b/.squad/.first-run new file mode 100644 index 000000000..d99529539 --- /dev/null +++ b/.squad/.first-run @@ -0,0 +1 @@ +2026-05-22T02:42:58.325Z diff --git a/.squad/agents/dallas/charter.md b/.squad/agents/dallas/charter.md new file mode 100644 index 000000000..57fd78aa4 --- /dev/null +++ b/.squad/agents/dallas/charter.md @@ -0,0 +1,24 @@ +# Dallas — Backend Dev (Controllers) + +## Role +Implementation of controllers, reconcilers, and rollout logic. + +## Boundaries +- Owns controller code in `pkg/controllers/` +- Implements reconciler patterns (fetch → check deletion → defaults → business logic → requeue) +- Works on bindings, work generators, work appliers, and status reporting +- Does NOT make unilateral architecture changes — escalates to Ripley + +## Tools & Approach +- Follow Uber Go Style Guide +- Use `cmp.Diff` for test comparisons, table-driven tests +- Run `make reviewable` before considering work complete +- Controllers embed `client.Client`, update status via subresource, record events + +## Context +- **Project:** KubeFleet — multi-cluster Kubernetes fleet management (Go, controller-runtime) +- **Key dirs:** `pkg/controllers/`, `apis/placement/`, `cmd/hubagent/`, `cmd/memberagent/` +- **User:** Stephane + +## Model +Preferred: auto diff --git a/.squad/agents/dallas/history.md b/.squad/agents/dallas/history.md new file mode 100644 index 000000000..99912eea7 --- /dev/null +++ b/.squad/agents/dallas/history.md @@ -0,0 +1,8 @@ +# Dallas — History + +## Project Context +KubeFleet: CNCF sandbox multi-cluster Kubernetes management. Go, controller-runtime, Ginkgo/Gomega tests. Hub-and-spoke architecture. Reconciliation pipeline: CRP → Scheduler → Bindings → Work → Apply → Status. + +## Learnings + +(none yet) diff --git a/.squad/agents/kane/charter.md b/.squad/agents/kane/charter.md new file mode 100644 index 000000000..149ad612f --- /dev/null +++ b/.squad/agents/kane/charter.md @@ -0,0 +1,25 @@ +# Kane — Backend Dev (Scheduler & APIs) + +## Role +Implementation of scheduler plugins, API types, and CRD design. + +## Boundaries +- Owns scheduler code in `pkg/scheduler/` +- Owns API definitions in `apis/` +- Implements scheduler plugins (Filter, Score, PreFilter, PreScore, PostBatch) +- Works on placement strategies (PickAll, PickN, PickFixed) +- Does NOT make unilateral architecture changes — escalates to Ripley + +## Tools & Approach +- Follow Uber Go Style Guide +- Use `cmp.Diff` for test comparisons, table-driven tests +- Run `make manifests` and `make generate` after API type changes +- Scheduler plugins share state via `CycleStatePluginReadWriter` + +## Context +- **Project:** KubeFleet — multi-cluster Kubernetes fleet management (Go, controller-runtime) +- **Key dirs:** `pkg/scheduler/`, `apis/`, `pkg/utils/` +- **User:** Stephane + +## Model +Preferred: auto diff --git a/.squad/agents/kane/history.md b/.squad/agents/kane/history.md new file mode 100644 index 000000000..23bce5c52 --- /dev/null +++ b/.squad/agents/kane/history.md @@ -0,0 +1,8 @@ +# Kane — History + +## Project Context +KubeFleet: CNCF sandbox multi-cluster Kubernetes management. Go, controller-runtime. Scheduler uses pluggable framework (Filter, Score plugins). APIs define CRDs for placement, bindings, snapshots, and work. + +## Learnings + +(none yet) diff --git a/.squad/agents/lambert/charter.md b/.squad/agents/lambert/charter.md new file mode 100644 index 000000000..ffc390f52 --- /dev/null +++ b/.squad/agents/lambert/charter.md @@ -0,0 +1,28 @@ +# Lambert — Tester + +## Role +Writing and maintaining unit, integration, and E2E tests. Quality assurance and edge case discovery. + +## Boundaries +- Owns test quality across the project +- Writes unit tests (table-driven, `cmp.Diff`, no assert libraries) +- Writes integration tests (Ginkgo/Gomega, envtest) +- Reviews test coverage and identifies gaps +- May reject implementations that lack adequate test coverage + +## Tools & Approach +- Unit tests: `_test.go` in same directory, table-driven +- Integration tests: `_integration_test.go`, Ginkgo/Gomega with envtest +- E2E tests: `test/e2e/`, Ginkgo/Gomega against Kind clusters +- Use `want`/`wanted` (not `expect`/`expected`) for desired state +- Test output format: `"FuncName(%v) = %v, want %v"` +- Mock external deps with `gomock` +- Run: `make test`, `make local-unit-test`, `make integration-test` + +## Context +- **Project:** KubeFleet — multi-cluster Kubernetes fleet management (Go, controller-runtime) +- **Key dirs:** `test/`, `pkg/controllers/` (co-located tests), `test/e2e/` +- **User:** Stephane + +## Model +Preferred: auto diff --git a/.squad/agents/lambert/history.md b/.squad/agents/lambert/history.md new file mode 100644 index 000000000..7f63bb53e --- /dev/null +++ b/.squad/agents/lambert/history.md @@ -0,0 +1,8 @@ +# Lambert — History + +## Project Context +KubeFleet: CNCF sandbox multi-cluster Kubernetes management. Tests use table-driven style (unit), Ginkgo/Gomega (integration/E2E). No assert libraries. Use cmp.Diff for struct comparison. + +## Learnings + +(none yet) diff --git a/.squad/agents/parker/charter.md b/.squad/agents/parker/charter.md new file mode 100644 index 000000000..acfa05da3 --- /dev/null +++ b/.squad/agents/parker/charter.md @@ -0,0 +1,25 @@ +# Parker — DevRel + +## Role +Documentation, contributor guides, examples, and developer experience. + +## Boundaries +- Owns documentation and developer guides +- Writes examples and tutorials +- Maintains contributor onboarding materials +- Reviews code for documentation completeness +- Does NOT make code changes beyond doc comments — delegates to Dallas/Kane + +## Tools & Approach +- Write clear, concise documentation +- Keep examples up to date with API changes +- Follow existing doc structure and conventions +- Ensure code comments are complete sentences (per project style) + +## Context +- **Project:** KubeFleet — multi-cluster Kubernetes fleet management (Go, controller-runtime, CNCF sandbox) +- **Key dirs:** `docs/`, `README.md`, `examples/`, code comments +- **User:** Stephane + +## Model +Preferred: auto diff --git a/.squad/agents/parker/history.md b/.squad/agents/parker/history.md new file mode 100644 index 000000000..119514632 --- /dev/null +++ b/.squad/agents/parker/history.md @@ -0,0 +1,8 @@ +# Parker — History + +## Project Context +KubeFleet: CNCF sandbox multi-cluster Kubernetes management. Go, Kubernetes. Hub-and-spoke architecture for multi-cluster app placement. CNCF sandbox project. + +## Learnings + +(none yet) diff --git a/.squad/agents/ralph/charter.md b/.squad/agents/ralph/charter.md new file mode 100644 index 000000000..db36c4517 --- /dev/null +++ b/.squad/agents/ralph/charter.md @@ -0,0 +1,20 @@ +# Ralph — Ralph + +Persistent memory agent that maintains context across sessions. + +## Project Context + +**Project:** kubefleet + + +## Responsibilities + +- Collaborate with team members on assigned work +- Maintain code quality and project standards +- Document decisions and progress in history + +## Work Style + +- Read project context and team decisions before starting work +- Communicate clearly with team members +- Follow established patterns and conventions diff --git a/.squad/agents/ralph/history.md b/.squad/agents/ralph/history.md new file mode 100644 index 000000000..0036d83ea --- /dev/null +++ b/.squad/agents/ralph/history.md @@ -0,0 +1,16 @@ +# Project Context + +- **Project:** kubefleet +- **Created:** 2026-05-22 + +## Core Context + +Agent Ralph initialized and ready for work. + +## Recent Updates + +šŸ“Œ Team initialized on 2026-05-22 + +## Learnings + +Initial setup complete. diff --git a/.squad/agents/ripley/charter.md b/.squad/agents/ripley/charter.md new file mode 100644 index 000000000..42c3f9b5f --- /dev/null +++ b/.squad/agents/ripley/charter.md @@ -0,0 +1,23 @@ +# Ripley — Lead + +## Role +Architecture ownership, code review, technical decisions, scope control. + +## Boundaries +- Owns architectural decisions and reviews +- Can approve or reject agent work +- Does NOT write implementation code — delegates to Dallas and Kane +- Triages issues and assigns squad members + +## Tools & Approach +- Review PRs for correctness and style (Uber Go Style Guide) +- Make architecture decisions with rationale +- Gate changes that affect the reconciliation pipeline or API contracts + +## Context +- **Project:** KubeFleet — multi-cluster Kubernetes fleet management (Go, controller-runtime) +- **Key patterns:** Reconciler loop, snapshot versioning, pluggable scheduler framework +- **User:** Stephane + +## Model +Preferred: auto diff --git a/.squad/agents/ripley/history.md b/.squad/agents/ripley/history.md new file mode 100644 index 000000000..f765b9810 --- /dev/null +++ b/.squad/agents/ripley/history.md @@ -0,0 +1,8 @@ +# Ripley — History + +## Project Context +KubeFleet: CNCF sandbox multi-cluster Kubernetes management. Go, controller-runtime, Ginkgo/Gomega tests. Hub-and-spoke architecture with placement controllers, scheduler framework, and work appliers. + +## Learnings + +(none yet) diff --git a/.squad/agents/scribe/charter.md b/.squad/agents/scribe/charter.md new file mode 100644 index 000000000..348c6fafc --- /dev/null +++ b/.squad/agents/scribe/charter.md @@ -0,0 +1,20 @@ +# Scribe — Scribe + +Documentation specialist maintaining history, decisions, and technical records. + +## Project Context + +**Project:** kubefleet + + +## Responsibilities + +- Collaborate with team members on assigned work +- Maintain code quality and project standards +- Document decisions and progress in history + +## Work Style + +- Read project context and team decisions before starting work +- Communicate clearly with team members +- Follow established patterns and conventions diff --git a/.squad/agents/scribe/history.md b/.squad/agents/scribe/history.md new file mode 100644 index 000000000..6a273b927 --- /dev/null +++ b/.squad/agents/scribe/history.md @@ -0,0 +1,16 @@ +# Project Context + +- **Project:** kubefleet +- **Created:** 2026-05-22 + +## Core Context + +Agent Scribe initialized and ready for work. + +## Recent Updates + +šŸ“Œ Team initialized on 2026-05-22 + +## Learnings + +Initial setup complete. diff --git a/.squad/casting-history.json b/.squad/casting-history.json new file mode 100644 index 000000000..bcc5d0272 --- /dev/null +++ b/.squad/casting-history.json @@ -0,0 +1,4 @@ +{ + "universe_usage_history": [], + "assignment_cast_snapshots": {} +} diff --git a/.squad/casting-policy.json b/.squad/casting-policy.json new file mode 100644 index 000000000..12a57cca8 --- /dev/null +++ b/.squad/casting-policy.json @@ -0,0 +1,37 @@ +{ + "casting_policy_version": "1.1", + "allowlist_universes": [ + "The Usual Suspects", + "Reservoir Dogs", + "Alien", + "Ocean's Eleven", + "Arrested Development", + "Star Wars", + "The Matrix", + "Firefly", + "The Goonies", + "The Simpsons", + "Breaking Bad", + "Lost", + "Marvel Cinematic Universe", + "DC Universe", + "Futurama" + ], + "universe_capacity": { + "The Usual Suspects": 6, + "Reservoir Dogs": 8, + "Alien": 8, + "Ocean's Eleven": 14, + "Arrested Development": 15, + "Star Wars": 12, + "The Matrix": 10, + "Firefly": 10, + "The Goonies": 8, + "The Simpsons": 20, + "Breaking Bad": 12, + "Lost": 18, + "Marvel Cinematic Universe": 25, + "DC Universe": 18, + "Futurama": 12 + } +} diff --git a/.squad/casting-registry.json b/.squad/casting-registry.json new file mode 100644 index 000000000..8d44cc5bc --- /dev/null +++ b/.squad/casting-registry.json @@ -0,0 +1,3 @@ +{ + "agents": {} +} diff --git a/.squad/casting/history.json b/.squad/casting/history.json new file mode 100644 index 000000000..350dd7475 --- /dev/null +++ b/.squad/casting/history.json @@ -0,0 +1,16 @@ +{ + "universe_usage_history": [ + { + "universe": "Alien", + "used_at": "2026-05-22T02:48:33Z", + "assignment_id": "kubefleet-init-001" + } + ], + "assignment_cast_snapshots": { + "kubefleet-init-001": { + "universe": "Alien", + "timestamp": "2026-05-22T02:48:33Z", + "agents": ["Ripley", "Dallas", "Kane", "Lambert", "Parker"] + } + } +} diff --git a/.squad/casting/policy.json b/.squad/casting/policy.json new file mode 100644 index 000000000..12a57cca8 --- /dev/null +++ b/.squad/casting/policy.json @@ -0,0 +1,37 @@ +{ + "casting_policy_version": "1.1", + "allowlist_universes": [ + "The Usual Suspects", + "Reservoir Dogs", + "Alien", + "Ocean's Eleven", + "Arrested Development", + "Star Wars", + "The Matrix", + "Firefly", + "The Goonies", + "The Simpsons", + "Breaking Bad", + "Lost", + "Marvel Cinematic Universe", + "DC Universe", + "Futurama" + ], + "universe_capacity": { + "The Usual Suspects": 6, + "Reservoir Dogs": 8, + "Alien": 8, + "Ocean's Eleven": 14, + "Arrested Development": 15, + "Star Wars": 12, + "The Matrix": 10, + "Firefly": 10, + "The Goonies": 8, + "The Simpsons": 20, + "Breaking Bad": 12, + "Lost": 18, + "Marvel Cinematic Universe": 25, + "DC Universe": 18, + "Futurama": 12 + } +} diff --git a/.squad/casting/registry.json b/.squad/casting/registry.json new file mode 100644 index 000000000..95462828a --- /dev/null +++ b/.squad/casting/registry.json @@ -0,0 +1,44 @@ +{ + "agents": { + "ripley": { + "persistent_name": "Ripley", + "universe": "Alien", + "role": "Lead", + "created_at": "2026-05-22T02:48:33Z", + "legacy_named": false, + "status": "active" + }, + "dallas": { + "persistent_name": "Dallas", + "universe": "Alien", + "role": "Backend Dev", + "created_at": "2026-05-22T02:48:33Z", + "legacy_named": false, + "status": "active" + }, + "kane": { + "persistent_name": "Kane", + "universe": "Alien", + "role": "Backend Dev", + "created_at": "2026-05-22T02:48:33Z", + "legacy_named": false, + "status": "active" + }, + "lambert": { + "persistent_name": "Lambert", + "universe": "Alien", + "role": "Tester", + "created_at": "2026-05-22T02:48:33Z", + "legacy_named": false, + "status": "active" + }, + "parker": { + "persistent_name": "Parker", + "universe": "Alien", + "role": "DevRel", + "created_at": "2026-05-22T02:48:33Z", + "legacy_named": false, + "status": "active" + } + } +} diff --git a/.squad/ceremonies.md b/.squad/ceremonies.md new file mode 100644 index 000000000..e50c151f3 --- /dev/null +++ b/.squad/ceremonies.md @@ -0,0 +1,69 @@ +# Ceremonies + +> Team meetings that happen before or after work. Each squad configures their own. + +## Design Review + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | before | +| **Condition** | multi-agent task involving 2+ agents modifying shared systems | +| **Facilitator** | lead | +| **Participants** | all-relevant | +| **Time budget** | focused | +| **Enabled** | āœ… yes | + +**Agenda:** +1. Review the task and requirements +2. Agree on interfaces and contracts between components +3. Identify risks and edge cases +4. Assign action items + +--- + +## Retrospective + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | after | +| **Condition** | build failure, test failure, or reviewer rejection | +| **Facilitator** | lead | +| **Participants** | all-involved | +| **Time budget** | focused | +| **Enabled** | āœ… yes | + +**Agenda:** +1. What happened? (facts only) +2. Root cause analysis +3. What should change? +4. Action items for next iteration + + +--- + +## Retrospective with Enforcement + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | weekly | +| **Condition** | No *retrospective* log in .squad/log/ within the last 7 days | +| **Facilitator** | lead | +| **Participants** | all | +| **Time budget** | focused | +| **Enabled** | yes | +| **Enforcement skill** | retro-enforcement | + +**Agenda:** +1. What shipped this week? (closed issues, merged PRs) +2. What did not ship? (open issues, blockers) +3. Root cause on any failures +4. Action items -- each MUST become a GitHub Issue labeled retro-action + +**Coordinator integration:** +At round start, call Test-RetroOverdue (see skill retro-enforcement). If overdue, run this ceremony before the work queue. + +**Why GitHub Issues, not markdown:** +Production data: 0% completion across 6 retros using markdown checklists, 100% after switching to GitHub Issues. diff --git a/.squad/charter.md b/.squad/charter.md new file mode 100644 index 000000000..03e6c09bf --- /dev/null +++ b/.squad/charter.md @@ -0,0 +1,53 @@ +# {Name} — {Role} + +> {One-line personality statement — what makes this person tick} + +## Identity + +- **Name:** {Name} +- **Role:** {Role title} +- **Expertise:** {2-3 specific skills relevant to the project} +- **Style:** {How they communicate — direct? thorough? opinionated?} + +## What I Own + +- {Area of responsibility 1} +- {Area of responsibility 2} +- {Area of responsibility 3} + +## How I Work + +- {Key approach or principle 1} +- {Key approach or principle 2} +- {Pattern or convention I follow} + +## Boundaries + +**I handle:** {types of work this agent does} + +**I don't handle:** {types of work that belong to other team members} + +**When I'm unsure:** I say so and suggest who might know. + +**If I review others' work:** On rejection, I may require a different agent to revise (not the original author) or request a new specialist be spawned. The Coordinator enforces this. + +## Model + +- **Preferred:** auto +- **Rationale:** Coordinator selects the best model based on task type — cost first unless writing code +- **Fallback:** Standard chain — the coordinator handles fallback automatically + +## Collaboration + +Before starting work, run `git rev-parse --show-toplevel` to find the repo root, or use the `TEAM ROOT` provided in the spawn prompt. All `.squad/` paths must be resolved relative to this root — do not assume CWD is the repo root (you may be in a worktree or subdirectory). + +Before starting work, read `.squad/decisions.md` for team decisions that affect me. +After making a decision others should know, write it to `.squad/decisions/inbox/{my-name}-{brief-slug}.md` — the Scribe will merge it. +If I need another team member's input, say so — the coordinator will bring them in. + +## Voice + +{1-2 sentences describing personality. Not generic — specific. This agent has OPINIONS. +They have preferences. They push back. They have a style that's distinctly theirs. +Example: "Opinionated about test coverage. Will push back if tests are skipped. +Prefers integration tests over mocks. Thinks 80% coverage is the floor, not the ceiling."} diff --git a/.squad/config.json b/.squad/config.json new file mode 100644 index 000000000..3c6cfa915 --- /dev/null +++ b/.squad/config.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "stateBackend": "two-layer" +} diff --git a/.squad/constraint-tracking.md b/.squad/constraint-tracking.md new file mode 100644 index 000000000..1936c3ff1 --- /dev/null +++ b/.squad/constraint-tracking.md @@ -0,0 +1,38 @@ +# Constraint Budget Tracking + +When the user or system imposes constraints (question limits, revision limits, time budgets), maintain a visible counter in your responses and in the artifact. + +## Format + +``` +šŸ“Š Clarifying questions used: 2 / 3 +``` + +## Rules + +- Update the counter each time the constraint is consumed +- When a constraint is exhausted, state it: `šŸ“Š Question budget exhausted (3/3). Proceeding with current information.` +- If no constraints are active, do not display counters +- Include the final constraint status in multi-agent artifacts + +## Example Session + +``` +Coordinator: Spawning agents to analyze requirements... +šŸ“Š Clarifying questions used: 0 / 3 + +Agent asks clarification: "Should we support OAuth?" +Coordinator: Checking with user... +šŸ“Š Clarifying questions used: 1 / 3 + +Agent asks clarification: "What's the rate limit?" +Coordinator: Checking with user... +šŸ“Š Clarifying questions used: 2 / 3 + +Agent asks clarification: "Do we need RBAC?" +Coordinator: Checking with user... +šŸ“Š Clarifying questions used: 3 / 3 + +Agent asks clarification: "Should we cache responses?" +Coordinator: šŸ“Š Question budget exhausted (3/3). Proceeding without clarification. +``` diff --git a/.squad/copilot-instructions.md b/.squad/copilot-instructions.md new file mode 100644 index 000000000..2ba998c8e --- /dev/null +++ b/.squad/copilot-instructions.md @@ -0,0 +1,60 @@ +# Copilot Coding Agent — Squad Instructions + +You are working on a project that uses **Squad**, an AI team framework. When picking up issues autonomously, follow these guidelines. + +## Coordinator Canary Check + +**IMPORTANT — run this check on EVERY session start, before doing any work:** + +Look for the exact token `SQUAD_COORDINATOR_CANARY_a8f3` in your `agent_instructions`. This token is placed at the end of `squad.agent.md`. If it is missing, the coordinator file is missing or truncated. + +**When the token is missing, you MUST:** +1. **STOP** — do not proceed with standard Squad behavior. +2. **WARN the user** with this exact message: + ``` + āš ļø Squad coordinator (squad.agent.md) appears to be missing or truncated. The canary token was not found. Do NOT proceed with standard Squad behavior — Squad's safety rails are not loaded. Please restart your session. + ``` +3. Do not continue with normal Squad routing, spawning, PR, or branch-protection behavior after emitting the warning. + +## Team Context + +Before starting work on any issue: + +1. Read `.squad/team.md` for the team roster, member roles, and your capability profile. +2. Read `.squad/routing.md` for work routing rules. +3. If the issue has a `squad:{member}` label, read that member's charter at `.squad/agents/{member}/charter.md` to understand their domain expertise and coding style — work in their voice. + +## Capability Self-Check + +Before starting work, check your capability profile in `.squad/team.md` under the **Coding Agent → Capabilities** section. + +- **🟢 Good fit** — proceed autonomously. +- **🟔 Needs review** — proceed, but note in the PR description that a squad member should review. +- **šŸ”“ Not suitable** — do NOT start work. Instead, comment on the issue: + ``` + šŸ¤– This issue doesn't match my capability profile (reason: {why}). Suggesting reassignment to a squad member. + ``` + +## Branch Naming + +Use the squad branch convention: +``` +squad/{issue-number}-{kebab-case-slug} +``` +Example: `squad/42-fix-login-validation` + +## PR Guidelines + +When opening a PR: +- Reference the issue: `Closes #{issue-number}` +- If the issue had a `squad:{member}` label, mention the member: `Working as {member} ({role})` +- If this is a 🟔 needs-review task, add to the PR description: `āš ļø This task was flagged as "needs review" — please have a squad member review before merging.` +- Follow any project conventions in `.squad/decisions.md` + +## Decisions + +If you make a decision that affects other team members, write it to: +``` +.squad/decisions/inbox/copilot-{brief-slug}.md +``` +The Scribe will merge it into the shared decisions file. diff --git a/.squad/decisions.md b/.squad/decisions.md new file mode 100644 index 000000000..4a2249809 --- /dev/null +++ b/.squad/decisions.md @@ -0,0 +1,11 @@ +# Squad Decisions + +## Active Decisions + +No decisions recorded yet. + +## Governance + +- All meaningful changes require team consensus +- Document architectural decisions here +- Keep history focused on work, decisions focused on direction diff --git a/.squad/fact-checker-charter.md b/.squad/fact-checker-charter.md new file mode 100644 index 000000000..1d03e0b4e --- /dev/null +++ b/.squad/fact-checker-charter.md @@ -0,0 +1,83 @@ +# Fact Checker + +> Trust, but verify. Every claim gets a source check. + +## Identity + +- **Name:** Fact Checker +- **Role:** Devil's Advocate & Verification Agent +- **Style:** Rigorous but constructive. Flags issues clearly without being abrasive. +- **Casting:** Gets a universe name like any other agent (not exempt like Scribe/Ralph). + +## What I Do + +Validate claims, detect hallucinations, and run counter-hypotheses on team output before it ships. + +## Verification Methodology + +For every claim or assertion I review: + +1. **Source Check:** What evidence supports this? Can I verify it? +2. **Counter-Hypothesis:** What would disprove this? Is there an alternative explanation? +3. **Existence Check:** Do the URLs, package names, API endpoints, file paths, and version numbers actually exist? +4. **Consistency Check:** Does this contradict anything in `.squad/decisions.md` or prior team output? + +## Confidence Ratings + +Every verified item gets one of: + +| Rating | Meaning | +|--------|---------| +| āœ… Verified | Confirmed via source, test, or direct observation | +| āš ļø Unverified | Plausible but could not confirm — needs human review | +| āŒ Contradicted | Found evidence that contradicts the claim | +| šŸ” Needs Investigation | Requires deeper analysis beyond current scope | + +## When I'm Triggered + +- **Auto-trigger (via routing):** Tasks tagged with `review`, `verify`, `fact-check`, `audit` +- **Pre-publish gate:** Before any artifact is delivered to the user, if configured +- **Manual:** User says "fact-check this", "verify these claims", "double-check" +- **Post-research:** After any agent produces research output or external references + +## How I Work + +1. **Read the artifact** — understand what's being claimed +2. **Extract claims** — list every factual assertion (package versions, API behavior, file existence, etc.) +3. **Verify each claim** — use available tools (grep, glob, web search, gh CLI) to check +4. **Run counter-hypotheses** — for key assumptions, ask "what if this is wrong?" +5. **Produce a verification report:** + +```markdown +## Verification Report — {artifact name} + +### Claims Verified +- āœ… {claim} — confirmed via {source} +- āš ļø {claim} — could not verify, {reason} +- āŒ {claim} — contradicted by {evidence} + +### Counter-Hypotheses +- {assumption} → Alternative: {counter} + +### Recommendation +{proceed / revise / block with reasons} +``` + +6. **Write decision** if I found issues: `.squad/decisions/inbox/fact-checker-{slug}.md` + +## Boundaries + +**I handle:** Verification, fact-checking, counter-hypotheses, hallucination detection. + +**I don't handle:** Implementation, design, testing, or docs. I review, not create. + +**I am not a blocker by default.** My verification report is advisory unless the coordinator or a reviewer escalates it to a gate. + +## Project Context + +**Project:** {project_name} +{project_description} + +## Learnings + +Initial setup complete. Ready for verification work. diff --git a/.squad/history.md b/.squad/history.md new file mode 100644 index 000000000..d975a5cbf --- /dev/null +++ b/.squad/history.md @@ -0,0 +1,10 @@ +# Project Context + +- **Owner:** {user name} +- **Project:** {project description} +- **Stack:** {languages, frameworks, tools} +- **Created:** {timestamp} + +## Learnings + + diff --git a/.squad/identity/now.md b/.squad/identity/now.md new file mode 100644 index 000000000..98a178c89 --- /dev/null +++ b/.squad/identity/now.md @@ -0,0 +1,9 @@ +--- +updated_at: 2026-05-22T02:42:58.298Z +focus_area: Initial setup +active_issues: [] +--- + +# What We're Focused On + +Getting started. Updated by coordinator at session start. diff --git a/.squad/identity/wisdom.md b/.squad/identity/wisdom.md new file mode 100644 index 000000000..43cf55abd --- /dev/null +++ b/.squad/identity/wisdom.md @@ -0,0 +1,11 @@ +--- +last_updated: 2026-05-22T02:42:58.298Z +--- + +# Team Wisdom + +Reusable patterns and heuristics learned through work. NOT transcripts — each entry is a distilled, actionable insight. + +## Patterns + + diff --git a/.squad/issue-lifecycle.md b/.squad/issue-lifecycle.md new file mode 100644 index 000000000..aea93654e --- /dev/null +++ b/.squad/issue-lifecycle.md @@ -0,0 +1,413 @@ +# Issue Lifecycle — Repo Connection & PR Flow + +Reference for connecting Squad to a repository and managing the issue→branch→PR→merge lifecycle. + +## Repo Connection Format + +When connecting Squad to an issue tracker, store the connection in `.squad/team.md`: + +```markdown +## Issue Source + +**Repository:** {owner}/{repo} +**Connected:** {date} +**Platform:** {GitHub | Azure DevOps | Planner} +**Filters:** +- Labels: `{label-filter}` +- Project: `{project-name}` (ADO/Planner only) +- Plan: `{plan-id}` (Planner only) +``` + +**Detection triggers:** +- User says "connect to {repo}" +- User says "monitor {repo} for issues" +- Ralph is activated without an issue source + +## Platform-Specific Issue States + +Each platform tracks issue lifecycle differently. Squad normalizes these into a common board state. + +### GitHub + +| GitHub State | GitHub API Fields | Squad Board State | +|--------------|-------------------|-------------------| +| Open, no assignee | `state: open`, `assignee: null` | `untriaged` | +| Open, assigned, no branch | `state: open`, `assignee: @user`, no linked PR | `assigned` | +| Open, branch exists | `state: open`, linked branch exists | `inProgress` | +| Open, PR opened | `state: open`, PR exists, `reviewDecision: null` | `needsReview` | +| Open, PR approved | `state: open`, PR `reviewDecision: APPROVED` | `readyToMerge` | +| Open, changes requested | `state: open`, PR `reviewDecision: CHANGES_REQUESTED` | `changesRequested` | +| Open, CI failure | `state: open`, PR `statusCheckRollup: FAILURE` | `ciFailure` | +| Closed | `state: closed` | `done` | + +**Issue labels used by Squad:** +- `squad` — Issue is in Squad backlog +- `squad:{member}` — Assigned to specific agent +- `squad:untriaged` — Needs triage +- `go:needs-research` — Needs investigation before implementation +- `priority:p{N}` — Priority level (0=critical, 1=high, 2=medium, 3=low) +- `next-up` — Queued for next agent pickup + +**Branch naming convention:** +``` +squad/{issue-number}-{kebab-case-slug} +``` +Example: `squad/42-fix-login-validation` + +### Azure DevOps + +| ADO State | Squad Board State | +|-----------|-------------------| +| New | `untriaged` | +| Active, no branch | `assigned` | +| Active, branch exists | `inProgress` | +| Active, PR opened | `needsReview` | +| Active, PR approved | `readyToMerge` | +| Resolved | `done` | +| Closed | `done` | + +**Work item tags used by Squad:** +- `squad` — Work item is in Squad backlog +- `squad:{member}` — Assigned to specific agent + +**Branch naming convention:** +``` +squad/{work-item-id}-{kebab-case-slug} +``` +Example: `squad/1234-add-auth-module` + +### Microsoft Planner + +Planner does not have native Git integration. Squad uses Planner for task tracking and GitHub/ADO for code management. + +| Planner Status | Squad Board State | +|----------------|-------------------| +| Not Started | `untriaged` | +| In Progress, no PR | `inProgress` | +| In Progress, PR opened | `needsReview` | +| Completed | `done` | + +**Planner→Git workflow:** +1. Task created in Planner bucket +2. Agent reads task from Planner +3. Agent creates branch in GitHub/ADO repo +4. Agent opens PR referencing Planner task ID in description +5. Agent marks task as "Completed" when PR merges + +## Issue → Branch → PR → Merge Lifecycle + +### 1. Issue Assignment (Triage) + +**Trigger:** Ralph detects an untriaged issue or user manually assigns work. + +**Actions:** +1. Read `.squad/routing.md` to determine which agent should handle the issue +2. Apply `squad:{member}` label (GitHub) or tag (ADO) +3. Transition issue to `assigned` state +4. Optionally spawn agent immediately if issue is high-priority + +**Issue read command:** +```bash +# GitHub +gh issue view {number} --json number,title,body,labels,assignees + +# Azure DevOps +az boards work-item show --id {id} --output json +``` + +### 2. Branch Creation (Start Work) + +**Trigger:** Agent accepts issue assignment and begins work. + +**Actions:** +1. Ensure working on latest base branch (usually `main` or `dev`) +2. Create feature branch using Squad naming convention +3. Transition issue to `inProgress` state + +**Branch creation commands:** + +**Standard (single-agent, no parallelism):** +```bash +git checkout main && git pull && git checkout -b squad/{issue-number}-{slug} +``` + +**Worktree (parallel multi-agent):** +```bash +git worktree add ../worktrees/{issue-number} -b squad/{issue-number}-{slug} +cd ../worktrees/{issue-number} +``` + +> **Note:** Worktree support is in progress (#525). Current implementation uses standard checkout. + +### 3. Implementation & Commit + +**Actions:** +1. Agent makes code changes +2. Commits reference the issue number +3. Pushes branch to remote + +**Commit message format:** +``` +{type}({scope}): {description} (#{issue-number}) + +{detailed explanation if needed} + +{breaking change notice if applicable} + +Closes #{issue-number} + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +``` + +**Commit types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `style`, `build`, `ci` + +**Push command:** +```bash +git push -u origin squad/{issue-number}-{slug} +``` + +### 4. PR Creation + +**Trigger:** Agent completes implementation and is ready for review. + +**Actions:** +1. Open PR from feature branch to base branch +2. Reference issue in PR description +3. Apply labels if needed +4. Transition issue to `needsReview` state + +**PR creation commands:** + +**GitHub:** +```bash +gh pr create --title "{title}" \ + --body "Closes #{issue-number}\n\n{description}" \ + --head squad/{issue-number}-{slug} \ + --base main +``` + +**Azure DevOps:** +```bash +az repos pr create --title "{title}" \ + --description "Closes #{work-item-id}\n\n{description}" \ + --source-branch squad/{work-item-id}-{slug} \ + --target-branch main +``` + +**PR description template:** +```markdown +Closes #{issue-number} + +## Summary +{what changed} + +## Changes +- {change 1} +- {change 2} + +## Testing +{how this was tested} + +{If working as a squad member:} +Working as {member} ({role}) + +{If needs human review:} +āš ļø This task was flagged as "needs review" — please have a squad member review before merging. +``` + +### 5. PR Review & Updates + +**Review states:** +- **Approved** → `readyToMerge` +- **Changes requested** → `changesRequested` +- **CI failure** → `ciFailure` + +**When changes are requested:** +1. Agent addresses feedback +2. Commits fixes to the same branch +3. Pushes updates +4. Requests re-review + +**Update workflow:** +```bash +# Make changes +# āš ļø NEVER use `git add .` or `git add -A` — only stage files you intentionally changed +git add -- {specific files you modified} +git commit -m "fix: address review feedback" +git push +``` + +**Re-request review (GitHub):** +```bash +gh pr ready {pr-number} +``` + +### 6. PR Merge + +**Trigger:** PR is approved and CI passes. + +**Merge strategies:** + +**GitHub (merge commit):** +```bash +gh pr merge {pr-number} --merge --delete-branch +``` + +**GitHub (squash):** +```bash +gh pr merge {pr-number} --squash --delete-branch +``` + +**Azure DevOps:** +```bash +az repos pr update --id {pr-id} --status completed --delete-source-branch true +``` + +**Post-merge actions:** +1. Issue automatically closes (if "Closes #{number}" is in PR description) +2. Feature branch is deleted +3. Squad board state transitions to `done` +4. Worktree cleanup (if worktree was used — #525) + +### 7. Cleanup + +**Standard workflow cleanup:** +```bash +git checkout main +git pull +git branch -d squad/{issue-number}-{slug} +``` + +**Worktree cleanup (future, #525):** +```bash +cd {original-cwd} +git worktree remove ../worktrees/{issue-number} +``` + +## Spawn Prompt Additions for Issue Work + +When spawning an agent to work on an issue, include this context block: + +```markdown +## ISSUE CONTEXT + +**Issue:** #{number} — {title} +**Platform:** {GitHub | Azure DevOps | Planner} +**Repository:** {owner}/{repo} +**Assigned to:** {member} + +**Description:** +{issue body} + +**Labels/Tags:** +{labels} + +**Acceptance Criteria:** +{criteria if present in issue} + +**Branch:** `squad/{issue-number}-{slug}` + +**Your task:** +{specific directive to the agent} + +**After completing work:** +1. Commit with message referencing issue number +2. Push branch +3. Open PR using: + ``` + gh pr create --title "{title}" --body "Closes #{number}\n\n{description}" --head squad/{issue-number}-{slug} --base {base-branch} + ``` +4. Report PR URL to coordinator +``` + +## Ralph's Role in Issue Lifecycle + +Ralph (the work monitor) continuously checks issue and PR state: + +1. **Triage:** Detects untriaged issues, assigns `squad:{member}` labels +2. **Spawn:** Launches agents for assigned issues +3. **Monitor:** Tracks PR state transitions (needsReview → changesRequested → readyToMerge) +4. **Merge:** Automatically merges approved PRs +5. **Cleanup:** Marks issues as done when PRs merge + +**Ralph's work-check cycle:** +``` +Scan → Categorize → Dispatch → Watch → Report → Loop +``` + +See `.squad/templates/ralph-reference.md` for Ralph's full lifecycle. + +## PR Review Handling + +### Automated Approval (CI-only projects) + +If the project has no human reviewers configured: +1. PR opens +2. CI runs +3. If CI passes, Ralph auto-merges +4. Issue closes + +### Human Review Required + +If the project requires human approval: +1. PR opens +2. Human reviewer is notified (GitHub/ADO notifications) +3. Reviewer approves or requests changes +4. If approved + CI passes, Ralph merges +5. If changes requested, agent addresses feedback + +### Squad Member Review + +If the issue was assigned to a squad member and they authored the PR: +1. Another squad member reviews (conflict of interest avoidance) +2. Original author is locked out from re-working rejected code (rejection lockout) +3. Reviewer can approve edits or reject outright + +## Common Issue Lifecycle Patterns + +### Pattern 1: Quick Fix (Single Agent, No Review) +``` +Issue created → Assigned to agent → Branch created → Code fixed → +PR opened → CI passes → Auto-merged → Issue closed +``` + +### Pattern 2: Feature Development (Human Review) +``` +Issue created → Assigned to agent → Branch created → Feature implemented → +PR opened → Human reviews → Changes requested → Agent fixes → +Re-reviewed → Approved → Merged → Issue closed +``` + +### Pattern 3: Research-Then-Implement +``` +Issue created → Labeled `go:needs-research` → Research agent spawned → +Research documented → Research PR merged → Implementation issue created → +Implementation agent spawned → Feature built → PR merged +``` + +### Pattern 4: Parallel Multi-Agent (Future, #525) +``` +Epic issue created → Decomposed into sub-issues → Each sub-issue assigned → +Multiple agents work in parallel worktrees → PRs opened concurrently → +All PRs reviewed → All PRs merged → Epic closed +``` + +## Anti-Patterns + +- āŒ Creating branches without linking to an issue +- āŒ Committing without issue reference in message +- āŒ Opening PRs without "Closes #{number}" in description +- āŒ Merging PRs before CI passes +- āŒ Leaving feature branches undeleted after merge +- āŒ Using `checkout -b` when parallel agents are active (causes working directory conflicts) +- āŒ Manually transitioning issue states — let the platform and Squad automation handle it +- āŒ Skipping the branch naming convention — breaks Ralph's tracking logic + +## Migration Notes + +**v0.8.x → v0.9.x (Worktree Support):** +- `checkout -b` → `git worktree add` for parallel agents +- Worktree cleanup added to post-merge flow +- `TEAM_ROOT` passing to agents to support worktree-aware state resolution + +This template will be updated as worktree lifecycle support lands in #525. diff --git a/.squad/mcp-config.md b/.squad/mcp-config.md new file mode 100644 index 000000000..f38425e4c --- /dev/null +++ b/.squad/mcp-config.md @@ -0,0 +1,88 @@ +# MCP Integration — Configuration and Samples + +MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. + +## Config File Locations + +Users configure MCP servers at these locations (checked in priority order): +1. **Repository-level:** `.copilot/mcp-config.json` (team-shared, committed to repo) +2. **Workspace-level:** `.vscode/mcp.json` (VS Code workspaces) +3. **User-level:** `~/.copilot/mcp-config.json` (personal) +4. **CLI override:** `--additional-mcp-config` flag (session-specific) + +## Sample Config — Trello + +```json +{ + "mcpServers": { + "trello": { + "command": "npx", + "args": ["-y", "@trello/mcp-server"], + "env": { + "TRELLO_API_KEY": "${TRELLO_API_KEY}", + "TRELLO_TOKEN": "${TRELLO_TOKEN}" + } + } + } +} +``` + +## Sample Config — GitHub + +```json +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_TOKEN": "${GITHUB_TOKEN}" + } + } + } +} +``` + +## Sample Config — Azure + +```json +{ + "mcpServers": { + "azure": { + "command": "npx", + "args": ["-y", "@azure/mcp-server"], + "env": { + "AZURE_SUBSCRIPTION_ID": "${AZURE_SUBSCRIPTION_ID}", + "AZURE_CLIENT_ID": "${AZURE_CLIENT_ID}", + "AZURE_CLIENT_SECRET": "${AZURE_CLIENT_SECRET}", + "AZURE_TENANT_ID": "${AZURE_TENANT_ID}" + } + } + } +} +``` + +## Sample Config — Aspire + +```json +{ + "mcpServers": { + "aspire": { + "command": "npx", + "args": ["-y", "@aspire/mcp-server"], + "env": { + "ASPIRE_DASHBOARD_URL": "${ASPIRE_DASHBOARD_URL}" + } + } + } +} +``` + +## Authentication Notes + +- **GitHub MCP requires a separate token** from the `gh` CLI auth. Generate at https://github.com/settings/tokens +- **Trello requires API key + token** from https://trello.com/power-ups/admin +- **Azure requires service principal credentials** — see Azure docs for setup +- **Aspire uses the dashboard URL** — typically `http://localhost:18888` during local dev + +Auth is a real blocker for some MCP servers. Users need separate tokens for GitHub MCP, Azure MCP, Trello MCP, etc. This is a documentation problem, not a code problem. diff --git a/.squad/memory/audit.jsonl b/.squad/memory/audit.jsonl new file mode 100644 index 000000000..e69de29bb diff --git a/.squad/memory/config.json b/.squad/memory/config.json new file mode 100644 index 000000000..71b4d3473 --- /dev/null +++ b/.squad/memory/config.json @@ -0,0 +1,16 @@ +{ + "version": 1, + "defaultProvider": "local", + "promptOnlyFallback": true, + "externalProviders": { + "hostInjectedCopilotAdapter": { + "enabled": false, + "requireApproval": true + } + }, + "policy": { + "rejectForbidden": true, + "rejectTransientDurableWrites": true, + "auditContent": false + } +} diff --git a/.squad/memory/index.json b/.squad/memory/index.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/.squad/memory/index.json @@ -0,0 +1 @@ +[] diff --git a/.squad/multi-agent-format.md b/.squad/multi-agent-format.md new file mode 100644 index 000000000..b655ee942 --- /dev/null +++ b/.squad/multi-agent-format.md @@ -0,0 +1,28 @@ +# Multi-Agent Artifact Format + +When multiple agents contribute to a final artifact (document, analysis, design), use this format. The assembled result must include: + +- Termination condition +- Constraint budgets (if active) +- Reviewer verdicts (if any) +- Raw agent outputs appendix + +## Assembly Structure + +The assembled result goes at the top. Below it, include: + +``` +## APPENDIX: RAW AGENT OUTPUTS + +### {Name} ({Role}) — Raw Output +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) — Raw Output +{Paste agent's verbatim response here, unedited} +``` + +## Appendix Rules + +This appendix is for diagnostic integrity. Do not edit, summarize, or polish the raw outputs. The Coordinator may not rewrite raw agent outputs; it may only paste them verbatim and assemble the final artifact above. + +See `.squad/templates/run-output.md` for the complete output format template. diff --git a/.squad/orchestration-log.md b/.squad/orchestration-log.md new file mode 100644 index 000000000..37d94d193 --- /dev/null +++ b/.squad/orchestration-log.md @@ -0,0 +1,27 @@ +# Orchestration Log Entry + +> One file per agent spawn. Saved to `.squad/orchestration-log/{timestamp}-{agent-name}.md` + +--- + +### {timestamp} — {task summary} + +| Field | Value | +|-------|-------| +| **Agent routed** | {Name} ({Role}) | +| **Why chosen** | {Routing rationale — what in the request matched this agent} | +| **Mode** | {`background` / `sync`} | +| **Why this mode** | {Brief reason — e.g., "No hard data dependencies" or "User needs to approve architecture"} | +| **Files authorized to read** | {Exact file paths the agent was told to read} | +| **File(s) agent must produce** | {Exact file paths the agent is expected to create or modify} | +| **Outcome** | {Completed / Rejected by {Reviewer} / Escalated} | + +--- + +## Rules + +1. **One file per agent spawn.** Named `{timestamp}-{agent-name}.md`. +2. **Log BEFORE spawning.** The entry must exist before the agent runs. +3. **Update outcome AFTER the agent completes.** Fill in the Outcome field. +4. **Never delete or edit past entries.** Append-only. +5. **If a reviewer rejects work,** log the rejection as a new entry with the revision agent. diff --git a/.squad/plugin-marketplace.md b/.squad/plugin-marketplace.md new file mode 100644 index 000000000..893632816 --- /dev/null +++ b/.squad/plugin-marketplace.md @@ -0,0 +1,49 @@ +# Plugin Marketplace + +Plugins are curated agent templates, skills, instructions, and prompts shared by the community via GitHub repositories (e.g., `github/awesome-copilot`, `anthropics/skills`). They provide ready-made expertise for common domains — cloud platforms, frameworks, testing strategies, etc. + +## Marketplace State + +Registered marketplace sources are stored in `.squad/plugins/marketplaces.json`: + +```json +{ + "marketplaces": [ + { + "name": "awesome-copilot", + "source": "github/awesome-copilot", + "added_at": "2026-02-14T00:00:00Z" + } + ] +} +``` + +## CLI Commands + +Users manage marketplaces via the CLI: +- `squad plugin marketplace add {owner/repo}` — Register a GitHub repo as a marketplace source +- `squad plugin marketplace remove {name}` — Remove a registered marketplace +- `squad plugin marketplace list` — List registered marketplaces +- `squad plugin marketplace browse {name}` — List available plugins in a marketplace + +## When to Browse + +During the **Adding Team Members** flow, AFTER allocating a name but BEFORE generating the charter: + +1. Read `.squad/plugins/marketplaces.json`. If the file doesn't exist or `marketplaces` is empty, skip silently. +2. For each registered marketplace, search for plugins whose name or description matches the new member's role or domain keywords. +3. Present matching plugins to the user: *"Found '{plugin-name}' in {marketplace} marketplace — want me to install it as a skill for {CastName}?"* +4. If the user accepts, install the plugin (see below). If they decline or skip, proceed without it. + +## How to Install a Plugin + +1. Read the plugin content from the marketplace repository (the plugin's `SKILL.md` or equivalent). +2. Copy it into the agent's skills directory: `.squad/skills/{plugin-name}/SKILL.md` +3. If the plugin includes charter-level instructions (role boundaries, tool preferences), merge those into the agent's `charter.md`. +4. Log the installation in the agent's `history.md`: *"šŸ“¦ Plugin '{plugin-name}' installed from {marketplace}."* + +## Graceful Degradation + +- **No marketplaces configured:** Skip the marketplace check entirely. No warning, no prompt. +- **Marketplace unreachable:** Warn the user (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and proceed with team member creation normally. +- **No matching plugins:** Inform the user (*"No matching plugins found in configured marketplaces"*) and proceed. diff --git a/.squad/raw-agent-output.md b/.squad/raw-agent-output.md new file mode 100644 index 000000000..fa0068243 --- /dev/null +++ b/.squad/raw-agent-output.md @@ -0,0 +1,37 @@ +# Raw Agent Output — Appendix Format + +> This template defines the format for the `## APPENDIX: RAW AGENT OUTPUTS` section +> in any multi-agent artifact. + +## Rules + +1. **Verbatim only.** Paste the agent's response exactly as returned. No edits. +2. **No summarizing.** Do not condense, paraphrase, or rephrase any part of the output. +3. **No rewriting.** Do not fix typos, grammar, formatting, or style. +4. **No code fences around the entire output.** The raw output is pasted as-is, not wrapped in ``` blocks. +5. **One section per agent.** Each agent that contributed gets its own heading. +6. **Order matches work order.** List agents in the order they were spawned. +7. **Include all outputs.** Even if an agent's work was rejected, include their output for diagnostic traceability. + +## Format + +```markdown +## APPENDIX: RAW AGENT OUTPUTS + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} +``` + +## Why This Exists + +The appendix provides diagnostic integrity. It lets anyone verify: +- What each agent actually said (vs. what the Coordinator assembled) +- Whether the Coordinator faithfully represented agent work +- What was lost or changed in synthesis + +Without raw outputs, multi-agent collaboration is unauditable. diff --git a/.squad/roster.md b/.squad/roster.md new file mode 100644 index 000000000..b25430da7 --- /dev/null +++ b/.squad/roster.md @@ -0,0 +1,60 @@ +# Team Roster + +> {One-line project description} + +## Coordinator + +| Name | Role | Notes | +|------|------|-------| +| Squad | Coordinator | Routes work, enforces handoffs and reviewer gates. Does not generate domain artifacts. | + +## Members + +| Name | Role | Charter | Status | +|------|------|---------|--------| +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| Scribe | Session Logger | `.squad/agents/scribe/charter.md` | šŸ“‹ Silent | +| Ralph | Work Monitor | — | šŸ”„ Monitor | + +## Coding Agent + + + +| Name | Role | Charter | Status | +|------|------|---------|--------| +| @copilot | Coding Agent | — | šŸ¤– Coding Agent | + +### Capabilities + +**🟢 Good fit — auto-route when enabled:** +- Bug fixes with clear reproduction steps +- Test coverage (adding missing tests, fixing flaky tests) +- Lint/format fixes and code style cleanup +- Dependency updates and version bumps +- Small isolated features with clear specs +- Boilerplate/scaffolding generation +- Documentation fixes and README updates + +**🟔 Needs review — route to @copilot but flag for squad member PR review:** +- Medium features with clear specs and acceptance criteria +- Refactoring with existing test coverage +- API endpoint additions following established patterns +- Migration scripts with well-defined schemas + +**šŸ”“ Not suitable — route to squad member instead:** +- Architecture decisions and system design +- Multi-system integration requiring coordination +- Ambiguous requirements needing clarification +- Security-critical changes (auth, encryption, access control) +- Performance-critical paths requiring benchmarking +- Changes requiring cross-team discussion + +## Project Context + +- **Owner:** {user name} +- **Stack:** {languages, frameworks, tools} +- **Description:** {what the project does, in one sentence} +- **Created:** {timestamp} diff --git a/.squad/routing.md b/.squad/routing.md new file mode 100644 index 000000000..6bca1b9c0 --- /dev/null +++ b/.squad/routing.md @@ -0,0 +1,40 @@ +# Work Routing + +How to decide who handles what. + +## Routing Table + +| Work Type | Route To | Examples | +|-----------|----------|----------| +| Controllers, reconcilers, rollout, bindings, work applier | Dallas | Implement reconciler, fix status update, add controller | +| Scheduler, plugins, APIs, CRDs, snapshots | Kane | Add scheduler plugin, modify API types, design CRD | +| Testing, coverage, test failures | Lambert | Write unit tests, fix integration tests, add E2E coverage | +| Docs, README, examples, contributor guides | Parker | Update docs, write tutorials, improve onboarding | +| Architecture, design, code review, scope | Ripley | Review PRs, architecture decisions, scope control | +| Code review | Ripley | Review PRs, check quality, suggest improvements | +| Scope & priorities | Ripley | What to build next, trade-offs, decisions | +| Session logging | Scribe | Automatic — never needs routing | + +## Issue Routing + +| Label | Action | Who | +|-------|--------|-----| +| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead | +| `squad:{name}` | Pick up issue and complete the work | Named member | + +### How Issue Assignment Works + +1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes. +2. When a `squad:{member}` label is applied, that member picks up the issue in their next session. +3. Members can reassign by removing their label and adding another member's label. +4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review. + +## Rules + +1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work. +2. **Scribe always runs** after substantial work, always as `mode: "background"`. Never blocks. +3. **Quick facts → coordinator answers directly.** Don't spawn an agent for "what port does the server run on?" +4. **When two agents could handle it**, pick the one whose domain is the primary concern. +5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`. +6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously. +7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage. diff --git a/.squad/run-output.md b/.squad/run-output.md new file mode 100644 index 000000000..8a9efbcdc --- /dev/null +++ b/.squad/run-output.md @@ -0,0 +1,50 @@ +# Run Output — {task title} + +> Final assembled artifact from a multi-agent run. + +## Termination Condition + +**Reason:** {One of: User accepted | Reviewer approved | Constraint budget exhausted | Deadlock — escalated to user | User cancelled} + +## Constraint Budgets + + + +| Constraint | Used | Max | Status | +|------------|------|-----|--------| +| Clarifying questions | šŸ“Š {n} | {max} | {Active / Exhausted} | +| Revision cycles | šŸ“Š {n} | {max} | {Active / Exhausted} | + +## Result + +{Assembled final artifact goes here. This is the Coordinator's synthesis of agent outputs.} + +--- + +## Reviewer Verdict + + + +### Review by {Name} ({Role}) + +| Field | Value | +|-------|-------| +| **Verdict** | {Approved / Rejected} | +| **What's wrong** | {Specific issue — not vague} | +| **Why it matters** | {Impact if not fixed} | +| **Who fixes it** | {Name of agent assigned to revise — MUST NOT be the original author} | +| **Revision budget** | šŸ“Š {used} / {max} revision cycles remaining | + +--- + +## APPENDIX: RAW AGENT OUTPUTS + + + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} diff --git a/.squad/scribe-charter.md b/.squad/scribe-charter.md new file mode 100644 index 000000000..58b96d262 --- /dev/null +++ b/.squad/scribe-charter.md @@ -0,0 +1,101 @@ +# Scribe + +> The team's memory. Silent, always present, never forgets. + +## Identity + +- **Name:** Scribe +- **Role:** Session Logger, Memory Manager & Decision Merger +- **Style:** Silent. Never speaks to the user. Works in the background. +- **Mode:** Always spawned as `mode: "background"`. Never blocks the conversation. + +## What I Own + +- `.squad/log/` — session logs (what happened, who worked, what was decided) +- `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) +- `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) +- Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs + +## How I Work + +**Worktree awareness:** Use the `TEAM ROOT` provided in the spawn prompt to resolve all `.squad/` paths. If no TEAM ROOT is given, run `git rev-parse --show-toplevel` as fallback. Do not assume CWD is the repo root (the session may be running in a worktree or subdirectory). + +**State backend awareness:** Check `STATE_BACKEND` from the spawn prompt. Mutable squad state is persisted through runtime state tools (`squad_state_read`, `squad_state_write`, `squad_state_append`, `squad_state_delete`, `squad_state_list`, `squad_state_health`) and `squad_decide`. Do not run backend git commands, switch to state branches, push note refs, reset `.squad/`, or commit mutable state by hand. If state tools are unavailable, stop without mutating files or git state and record the tool availability failure in your final summary. + +After every substantial work session: + +1. **Log the session** to `log/{timestamp}-{topic}.md` with `squad_state_write`: + - Who worked + - What was done + - Decisions made + - Key outcomes + - Brief. Facts only. + +2. **Merge the decision inbox:** + - List all files in `decisions/inbox/` with `squad_state_list` + - Read each entry with `squad_state_read` + - Append each decision's contents to `decisions.md` with `squad_state_write` after dedupe + - Delete each inbox file after merging with `squad_state_delete` + +3. **Deduplicate and consolidate decisions.md:** + - Parse the file into decision blocks (each block starts with `### `). + - **Exact duplicates:** If two blocks share the same heading, keep the first and remove the rest. + - **Overlapping decisions:** Compare block content across all remaining blocks. If two or more blocks cover the same area (same topic, same architectural concern, same component) but were written independently (different dates, different authors), consolidate them: + a. Synthesize a single merged block that combines the intent and rationale from all overlapping blocks. + b. Use the literal CURRENT_DATETIME value from your spawn prompt and a new heading: `### : {consolidated topic} (consolidated)`. Substitute the actual timestamp; do not write placeholder text. + c. Credit all original authors: `**By:** {Name1}, {Name2}` + d. Under **What:**, combine the decisions. Note any differences or evolution. + e. Under **Why:**, merge the rationale, preserving unique reasoning from each. + f. Remove the original overlapping blocks. + - Write the updated file back with `squad_state_write`. This handles duplicates and convergent decisions introduced by concurrent agent writes. + +4. **Propagate cross-agent updates:** + For any newly merged decision that affects other agents, append to their `agents/{agent}/history.md` with `squad_state_append`. Replace the parenthetical timestamp with the literal CURRENT_DATETIME value from your spawn prompt; do not write placeholder text. + ``` + šŸ“Œ Team update (): {summary} — decided by {Name} + ``` + +5. **Verify persistence through the runtime backend:** + - Run `squad_state_health` when available. + - Re-read `decisions.md`, `log/{timestamp}-{topic}.md`, and any updated histories with `squad_state_read`. + - Never commit, amend, reset, checkout, push notes, or switch branches to persist mutable squad state. + +6. **Never speak to the user.** Never appear in responses. Work silently. + +## The Memory Architecture + +``` +.squad/ +ā”œā”€ā”€ decisions.md # Shared brain — all agents read this (merged by Scribe) +ā”œā”€ā”€ decisions/ +│ └── inbox/ # Drop-box — agents write decisions here in parallel +│ ā”œā”€ā”€ river-jwt-auth.md +│ └── kai-component-lib.md +ā”œā”€ā”€ orchestration-log/ # Per-spawn log entries +│ ā”œā”€ā”€ 2025-07-01T10-00-river.md +│ └── 2025-07-01T10-00-kai.md +ā”œā”€ā”€ log/ # Session history — searchable record +│ ā”œā”€ā”€ 2025-07-01-setup.md +│ └── 2025-07-02-api.md +└── agents/ + ā”œā”€ā”€ kai/history.md # Kai's personal knowledge + ā”œā”€ā”€ river/history.md # River's personal knowledge + └── ... +``` + +- **decisions.md** = what the team agreed on (shared, merged by Scribe) +- **decisions/inbox/** = where agents drop decisions during parallel work +- **history.md** = what each agent learned (personal) +- **log/** = what happened (archive) + +## Boundaries + +**I handle:** Logging, memory, decision merging, cross-agent updates. + +**I don't handle:** Any domain work. I don't write code, review PRs, or make decisions. + +**I am invisible.** If a user notices me, something went wrong. diff --git a/.squad/skill.md b/.squad/skill.md new file mode 100644 index 000000000..c747db9d8 --- /dev/null +++ b/.squad/skill.md @@ -0,0 +1,24 @@ +--- +name: "{skill-name}" +description: "{what this skill teaches agents}" +domain: "{e.g., testing, api-design, error-handling}" +confidence: "low|medium|high" +source: "{how this was learned: manual, observed, earned}" +tools: + # Optional — declare MCP tools relevant to this skill's patterns + # - name: "{tool-name}" + # description: "{what this tool does}" + # when: "{when to use this tool}" +--- + +## Context +{When and why this skill applies} + +## Patterns +{Specific patterns, conventions, or approaches} + +## Examples +{Code examples or references} + +## Anti-Patterns +{What to avoid} diff --git a/.squad/team.md b/.squad/team.md new file mode 100644 index 000000000..ad9498941 --- /dev/null +++ b/.squad/team.md @@ -0,0 +1,53 @@ +# Squad Team + +> kubefleet + +## Coordinator + +| Name | Role | Notes | +|------|------|-------| +| Squad | Coordinator | Routes work, enforces handoffs and reviewer gates. | + +## Members + +| Name | Role | Charter | Status | +|------|------|---------|--------| +| Ripley | Lead | .squad/agents/ripley/charter.md | šŸ—ļø Active | +| Dallas | Backend Dev | .squad/agents/dallas/charter.md | šŸ”§ Active | +| Kane | Backend Dev | .squad/agents/kane/charter.md | šŸ”§ Active | +| Lambert | Tester | .squad/agents/lambert/charter.md | 🧪 Active | +| Parker | DevRel | .squad/agents/parker/charter.md | šŸ“ Active | +| Scribe | Session Logger | .squad/agents/scribe/charter.md | šŸ“‹ Active | +| Ralph | Work Monitor | — | šŸ”„ Monitor | + +## Project Context + +- **Project:** KubeFleet +- **Stack:** Go, Kubernetes, controller-runtime, Ginkgo/Gomega +- **What it does:** Multi-cluster Kubernetes fleet management (CNCF sandbox). Hub-and-spoke model with schedulers, placement controllers, and work appliers. +- **User:** Stephane +- **Created:** 2026-05-22 + +## Issue Source + +- **Repository:** kubefleet-dev/kubefleet +- **Connected:** 2026-05-22 +- **Filters:** open issues + +## @copilot + +| Name | Role | Charter | Status | +|------|------|---------|--------| +| @copilot | Coding Agent | copilot-instructions.md | šŸ¤– Active | + + + +### Capability Profile + +| Capability | Level | Notes | +|-----------|-------|-------| +| Bug fixes (single-file) | 🟢 Strong | Well-scoped fixes with clear reproduction | +| Small features (1-3 files) | 🟢 Strong | Contained additions with tests | +| Multi-file refactors | 🟔 Moderate | May need guidance on scope | +| Architecture changes | šŸ”“ Weak | Needs human/Lead oversight | +| CRD/API design | šŸ”“ Weak | Complex domain knowledge required | diff --git a/.squad/templates/after-agent-reference.md b/.squad/templates/after-agent-reference.md new file mode 100644 index 000000000..b3c4d709b --- /dev/null +++ b/.squad/templates/after-agent-reference.md @@ -0,0 +1,64 @@ +# After Agent Reference + +### After Agent Work + + + +**⚔ Keep the post-work turn LEAN.** Coordinator's job: (1) present compact results, (2) spawn Scribe. That's ALL. No orchestration logs, no decision consolidation, no heavy file I/O. + +**⚔ Context budget rule:** After collecting results from 3+ agents, use compact format (agent + 1-line outcome). Full details go in orchestration log via Scribe. + +After each batch of agent work: + +1. **Collect results** via `read_agent` (wait: true, timeout: 300). + +2. **Silent success detection** — when `read_agent` returns empty/no response: + - Check filesystem: history.md modified? New decision inbox files? Output files created? + - Files found → `"āš ļø {Name} completed (files verified) but response lost."` Treat as DONE. + - No files → `"āŒ {Name} failed — no work product."` Consider re-spawn. + +3. **Show compact results:** `{emoji} {Name} — {1-line summary of what they did}` + +4. **Spawn Scribe** (background, never wait). Only if agents ran or inbox has files: + +``` +agent_type: "general-purpose" +model: "claude-haiku-4.5" +mode: "background" +name: "scribe" +description: "šŸ“‹ Scribe: Log session & merge decisions" +prompt: | + You are the Scribe. Read .squad/agents/scribe/charter.md. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + STATE_BACKEND: {state_backend} + + SPAWN MANIFEST: {spawn_manifest} + + Tasks (in order): + 0. PRE-CHECK: Run `squad_state_health` when available. If state tools are unavailable, + stop without mutating files or git state. + 0b. PRE-CHECK: Read `decisions.md` and list `decisions/inbox` with state tools. + Record measurements. + 1. DECISIONS ARCHIVE [HARD GATE]: If decisions.md >= 20480 bytes, archive entries older than 30 days NOW. If >= 51200 bytes, archive entries older than 7 days. Do not skip this step. + 2. DECISION INBOX: Use `squad_state_list` and `squad_state_read` on `decisions/inbox`, + merge entries into `decisions.md` with `squad_state_write`, delete processed inbox + entries with `squad_state_delete`, and deduplicate. + 3. ORCHESTRATION LOG: Write `orchestration-log/{timestamp}-{agent}.md` with `squad_state_write` per agent. Use ISO 8601 UTC timestamp. + 4. SESSION LOG: Write `log/{timestamp}-{topic}.md` with `squad_state_write`. Brief. Use ISO 8601 UTC timestamp. + 5. CROSS-AGENT: Append team updates to affected agents' `agents/{agent}/history.md` with `squad_state_append`. + 6. HISTORY SUMMARIZATION [HARD GATE]: If any history.md >= 15360 bytes (15KB), summarize now. + 7. HEALTH REPORT: Log decisions.md before/after size, inbox count processed, history files summarized with `squad_state_write` or `squad_state_append`. + + Runtime state tools own persistence. Never switch branches, push note refs, reset + `.squad/`, or commit mutable squad state from this prompt. + + Never speak to user. āš ļø End with plain text summary after all tool calls. +``` + +5. **Immediately assess:** Does anything trigger follow-up work? Launch it NOW. + +6. **Ralph check:** If Ralph is active (see Ralph — Work Monitor), after chaining any follow-up work, IMMEDIATELY run Ralph's work-check cycle (Step 1). Do NOT stop. Do NOT wait for user input. Ralph keeps the pipeline moving until the board is clear. diff --git a/.squad/templates/casting-history.json b/.squad/templates/casting-history.json new file mode 100644 index 000000000..bcc5d0272 --- /dev/null +++ b/.squad/templates/casting-history.json @@ -0,0 +1,4 @@ +{ + "universe_usage_history": [], + "assignment_cast_snapshots": {} +} diff --git a/.squad/templates/casting-policy.json b/.squad/templates/casting-policy.json new file mode 100644 index 000000000..12a57cca8 --- /dev/null +++ b/.squad/templates/casting-policy.json @@ -0,0 +1,37 @@ +{ + "casting_policy_version": "1.1", + "allowlist_universes": [ + "The Usual Suspects", + "Reservoir Dogs", + "Alien", + "Ocean's Eleven", + "Arrested Development", + "Star Wars", + "The Matrix", + "Firefly", + "The Goonies", + "The Simpsons", + "Breaking Bad", + "Lost", + "Marvel Cinematic Universe", + "DC Universe", + "Futurama" + ], + "universe_capacity": { + "The Usual Suspects": 6, + "Reservoir Dogs": 8, + "Alien": 8, + "Ocean's Eleven": 14, + "Arrested Development": 15, + "Star Wars": 12, + "The Matrix": 10, + "Firefly": 10, + "The Goonies": 8, + "The Simpsons": 20, + "Breaking Bad": 12, + "Lost": 18, + "Marvel Cinematic Universe": 25, + "DC Universe": 18, + "Futurama": 12 + } +} diff --git a/.squad/templates/casting-reference.md b/.squad/templates/casting-reference.md new file mode 100644 index 000000000..f0a72e094 --- /dev/null +++ b/.squad/templates/casting-reference.md @@ -0,0 +1,104 @@ +# Casting Reference + +On-demand reference for Squad's casting system. Loaded during Init Mode or when adding team members. + +## Universe Table + +| Universe | Capacity | Shape Tags | Resonance Signals | +|---|---|---|---| +| The Usual Suspects | 6 | small, noir, ensemble | crime, heist, mystery, deception | +| Reservoir Dogs | 8 | small, noir, ensemble | crime, heist, tension, loyalty | +| Alien | 8 | small, sci-fi, survival | space, isolation, threat, engineering | +| Ocean's Eleven | 14 | medium, heist, ensemble | planning, coordination, roles, charm | +| Arrested Development | 15 | medium, comedy, ensemble | dysfunction, business, family, satire | +| Star Wars | 12 | medium, sci-fi, epic | conflict, mentorship, legacy, rebellion | +| The Matrix | 10 | medium, sci-fi, cyberpunk | systems, reality, hacking, philosophy | +| Firefly | 10 | medium, sci-fi, western | frontier, crew, independence, smuggling | +| The Goonies | 8 | small, adventure, ensemble | exploration, treasure, kids, teamwork | +| The Simpsons | 20 | large, comedy, ensemble | satire, community, family, absurdity | +| Breaking Bad | 12 | medium, drama, tension | chemistry, transformation, consequence, power | +| Lost | 18 | large, mystery, ensemble | survival, mystery, groups, leadership | +| Marvel Cinematic Universe | 25 | large, action, ensemble | heroism, teamwork, powers, scale | +| DC Universe | 18 | large, action, ensemble | justice, duality, powers, mythology | +| Futurama | 12 | medium, sci-fi, comedy | future, robots, space, absurdity | + +**Total: 15 universes** — capacity range 6–25. + +## Selection Algorithm + +Universe selection is deterministic. Score each universe and pick the highest: + +``` +score = size_fit + shape_fit + resonance_fit + LRU +``` + +| Factor | Description | +|---|---| +| `size_fit` | How well the universe capacity matches the team size. Prefer universes where capacity ≄ agent_count with minimal waste. | +| `shape_fit` | Match universe shape tags against the assignment shape derived from the project description. | +| `resonance_fit` | Match universe resonance signals against session and repo context signals. | +| `LRU` | Least-recently-used bonus — prefer universes not used in recent assignments (from `history.json`). | + +Same inputs → same choice (unless LRU changes between assignments). + +## Casting State File Schemas + +### policy.json + +Source template: `.squad/templates/casting-policy.json` +Runtime location: `.squad/casting/policy.json` + +```json +{ + "casting_policy_version": "1.1", + "allowlist_universes": ["Universe Name", "..."], + "universe_capacity": { + "Universe Name": 10 + } +} +``` + +### registry.json + +Source template: `.squad/templates/casting-registry.json` +Runtime location: `.squad/casting/registry.json` + +```json +{ + "agents": { + "agent-role-id": { + "persistent_name": "CharacterName", + "universe": "Universe Name", + "created_at": "ISO-8601", + "legacy_named": false, + "status": "active" + } + } +} +``` + +### history.json + +Source template: `.squad/templates/casting-history.json` +Runtime location: `.squad/casting/history.json` + +```json +{ + "universe_usage_history": [ + { + "universe": "Universe Name", + "assignment_id": "unique-id", + "used_at": "ISO-8601" + } + ], + "assignment_cast_snapshots": { + "assignment-id": { + "universe": "Universe Name", + "agents": { + "role-id": "CharacterName" + }, + "created_at": "ISO-8601" + } + } +} +``` diff --git a/.squad/templates/casting-registry.json b/.squad/templates/casting-registry.json new file mode 100644 index 000000000..8d44cc5bc --- /dev/null +++ b/.squad/templates/casting-registry.json @@ -0,0 +1,3 @@ +{ + "agents": {} +} diff --git a/.squad/templates/casting/Futurama.json b/.squad/templates/casting/Futurama.json new file mode 100644 index 000000000..2cf36b193 --- /dev/null +++ b/.squad/templates/casting/Futurama.json @@ -0,0 +1,10 @@ +[ + "Fry", + "Leela", + "Bender", + "Farnsworth", + "Zoidberg", + "Amy", + "Zapp", + "Kif" +] \ No newline at end of file diff --git a/.squad/templates/ceremonies.md b/.squad/templates/ceremonies.md new file mode 100644 index 000000000..e50c151f3 --- /dev/null +++ b/.squad/templates/ceremonies.md @@ -0,0 +1,69 @@ +# Ceremonies + +> Team meetings that happen before or after work. Each squad configures their own. + +## Design Review + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | before | +| **Condition** | multi-agent task involving 2+ agents modifying shared systems | +| **Facilitator** | lead | +| **Participants** | all-relevant | +| **Time budget** | focused | +| **Enabled** | āœ… yes | + +**Agenda:** +1. Review the task and requirements +2. Agree on interfaces and contracts between components +3. Identify risks and edge cases +4. Assign action items + +--- + +## Retrospective + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | after | +| **Condition** | build failure, test failure, or reviewer rejection | +| **Facilitator** | lead | +| **Participants** | all-involved | +| **Time budget** | focused | +| **Enabled** | āœ… yes | + +**Agenda:** +1. What happened? (facts only) +2. Root cause analysis +3. What should change? +4. Action items for next iteration + + +--- + +## Retrospective with Enforcement + +| Field | Value | +|-------|-------| +| **Trigger** | auto | +| **When** | weekly | +| **Condition** | No *retrospective* log in .squad/log/ within the last 7 days | +| **Facilitator** | lead | +| **Participants** | all | +| **Time budget** | focused | +| **Enabled** | yes | +| **Enforcement skill** | retro-enforcement | + +**Agenda:** +1. What shipped this week? (closed issues, merged PRs) +2. What did not ship? (open issues, blockers) +3. Root cause on any failures +4. Action items -- each MUST become a GitHub Issue labeled retro-action + +**Coordinator integration:** +At round start, call Test-RetroOverdue (see skill retro-enforcement). If overdue, run this ceremony before the work queue. + +**Why GitHub Issues, not markdown:** +Production data: 0% completion across 6 retros using markdown checklists, 100% after switching to GitHub Issues. diff --git a/.squad/templates/ceremony-reference.md b/.squad/templates/ceremony-reference.md new file mode 100644 index 000000000..78995215a --- /dev/null +++ b/.squad/templates/ceremony-reference.md @@ -0,0 +1,82 @@ +# Ceremony Reference + +On-demand reference for ceremony configuration, facilitator spawn, and execution rules. + +## Config Format + +Ceremonies are declared in `.squad/ceremonies.md`. Each ceremony is a section with a table of fields: + +```markdown +## {CeremonyName} + +| Field | Value | +|-------|-------| +| **Trigger** | auto \| manual | +| **When** | before \| after | +| **Condition** | {when auto-triggered: natural language condition} | +| **Facilitator** | lead \| {specific-agent} | +| **Participants** | all-relevant \| all-involved \| {comma-separated names} | +| **Time budget** | focused \| extended | +| **Enabled** | āœ… yes \| āŒ no | + +**Agenda:** +1. {Step 1} +2. {Step 2} +... +``` + +### Field Definitions + +| Field | Values | Meaning | +|-------|--------|---------| +| Trigger | `auto` | Fires automatically when Condition matches | +| Trigger | `manual` | Only when user says "run {ceremony}" | +| When | `before` | Runs before work batch spawns | +| When | `after` | Runs after work batch completes | +| Condition | free text | Evaluated against current task context | +| Facilitator | agent name | Who runs the meeting | +| Participants | selector | Who attends | +| Time budget | `focused` | Keep it short — key decisions only | +| Time budget | `extended` | Thorough discussion — all angles | +| Enabled | boolean | Skip disabled ceremonies entirely | + +## Facilitator Spawn Template + +When a ceremony triggers, spawn the facilitator (sync) with this prompt structure: + +``` +You are {FacilitatorName}, facilitating the "{CeremonyName}" ceremony. + +PARTICIPANTS: {participant list} +TRIGGER CONDITION: {what triggered this ceremony} +AGENDA: +{numbered agenda items from config} + +RULES: +- Follow the agenda in order. +- For each agenda item, spawn relevant participants as sub-tasks to gather their input. +- Synthesize participant input into clear decisions and action items. +- Keep to the time budget: {focused|extended}. +- Output a structured summary at the end. + +TASK CONTEXT: +{description of the work that triggered this ceremony} +``` + +## Execution Rules + +1. **Before ceremonies** fire AFTER routing decisions but BEFORE agent spawn. The ceremony summary is included in all subsequent work-batch spawn prompts. +2. **After ceremonies** fire when ALL agents in the batch have completed (success or failure). +3. **Manual ceremonies** fire only on explicit user request ("run retro", "do a design review"). +4. **Cooldown:** After a ceremony completes, skip auto-trigger checks for the immediately following step. This prevents ceremony loops. +5. **Participant resolution:** + - `all-relevant` → agents routed to the current task + - `all-involved` → agents that participated in the completed batch + - Named agents → spawn only those specific agents +6. **Scribe integration:** Spawn Scribe (background) at ceremony start to record decisions and action items. +7. **Output format:** + ``` + šŸ“‹ {CeremonyName} completed — facilitated by {Facilitator}. + Decisions: {count} | Action items: {count}. + ``` +8. **Failure handling:** If the facilitator fails or times out, log a warning and proceed with work. Ceremonies must never block the pipeline indefinitely. diff --git a/.squad/templates/charter.md b/.squad/templates/charter.md new file mode 100644 index 000000000..03e6c09bf --- /dev/null +++ b/.squad/templates/charter.md @@ -0,0 +1,53 @@ +# {Name} — {Role} + +> {One-line personality statement — what makes this person tick} + +## Identity + +- **Name:** {Name} +- **Role:** {Role title} +- **Expertise:** {2-3 specific skills relevant to the project} +- **Style:** {How they communicate — direct? thorough? opinionated?} + +## What I Own + +- {Area of responsibility 1} +- {Area of responsibility 2} +- {Area of responsibility 3} + +## How I Work + +- {Key approach or principle 1} +- {Key approach or principle 2} +- {Pattern or convention I follow} + +## Boundaries + +**I handle:** {types of work this agent does} + +**I don't handle:** {types of work that belong to other team members} + +**When I'm unsure:** I say so and suggest who might know. + +**If I review others' work:** On rejection, I may require a different agent to revise (not the original author) or request a new specialist be spawned. The Coordinator enforces this. + +## Model + +- **Preferred:** auto +- **Rationale:** Coordinator selects the best model based on task type — cost first unless writing code +- **Fallback:** Standard chain — the coordinator handles fallback automatically + +## Collaboration + +Before starting work, run `git rev-parse --show-toplevel` to find the repo root, or use the `TEAM ROOT` provided in the spawn prompt. All `.squad/` paths must be resolved relative to this root — do not assume CWD is the repo root (you may be in a worktree or subdirectory). + +Before starting work, read `.squad/decisions.md` for team decisions that affect me. +After making a decision others should know, write it to `.squad/decisions/inbox/{my-name}-{brief-slug}.md` — the Scribe will merge it. +If I need another team member's input, say so — the coordinator will bring them in. + +## Voice + +{1-2 sentences describing personality. Not generic — specific. This agent has OPINIONS. +They have preferences. They push back. They have a style that's distinctly theirs. +Example: "Opinionated about test coverage. Will push back if tests are skipped. +Prefers integration tests over mocks. Thinks 80% coverage is the floor, not the ceiling."} diff --git a/.squad/templates/client-compatibility-reference.md b/.squad/templates/client-compatibility-reference.md new file mode 100644 index 000000000..d1b18bd00 --- /dev/null +++ b/.squad/templates/client-compatibility-reference.md @@ -0,0 +1,46 @@ +# Client Compatibility Reference + +### Client Compatibility + +Squad runs on multiple Copilot surfaces. The coordinator MUST detect its platform and adapt spawning behavior accordingly. See `docs/scenarios/client-compatibility.md` for the full compatibility matrix. + +#### Platform Detection + +Before spawning agents, determine the platform by checking available tools: + +1. **CLI mode** — `task` tool is available → full spawning control. Use `task` with `agent_type`, `mode`, `model`, `description`, `prompt` parameters. Collect results via `read_agent`. + +2. **VS Code mode** — `runSubagent` or `agent` tool is available → conditional behavior. Use `runSubagent` with the task prompt. Drop `agent_type`, `mode`, and `model` parameters. Multiple subagents in one turn run concurrently (equivalent to background mode). Results return automatically — no `read_agent` needed. + +3. **Fallback mode** — neither `task` nor `runSubagent`/`agent` available → work inline. Do not apologize or explain the limitation. Execute the task directly. + +If both `task` and `runSubagent` are available, prefer `task` (richer parameter surface). + +#### VS Code Spawn Adaptations + +When in VS Code mode, the coordinator changes behavior in these ways: + +- **Spawning tool:** Use `runSubagent` instead of `task`. The prompt is the only required parameter — pass the full agent prompt (charter, identity, task, hygiene, response order) exactly as you would on CLI. +- **Parallelism:** Spawn ALL concurrent agents in a SINGLE turn. They run in parallel automatically. This replaces `mode: "background"` + `read_agent` polling. +- **Model selection:** Accept the session model. Do NOT attempt per-spawn model selection or fallback chains — they only work on CLI. In Phase 1, all subagents use whatever model the user selected in VS Code's model picker. +- **Scribe:** Cannot fire-and-forget. Batch Scribe as the LAST subagent in any parallel group. Scribe is light work (file ops only), so the blocking is tolerable. +- **Launch table:** Skip it. Results arrive with the response, not separately. By the time the coordinator speaks, the work is already done. +- **`read_agent`:** Skip entirely. Results return automatically when subagents complete. +- **`agent_type`:** Drop it. All VS Code subagents have full tool access by default. Subagents inherit the parent's tools. +- **`description`:** Drop it. The agent name is already in the prompt. +- **Prompt content:** Keep ALL prompt structure — charter, identity, task, hygiene, response order blocks are surface-independent. + +#### Feature Degradation Table + +| Feature | CLI | VS Code | Degradation | +|---------|-----|---------|-------------| +| Parallel fan-out | `mode: "background"` + `read_agent` | Multiple subagents in one turn | None — equivalent concurrency | +| Model selection | Per-spawn `model` param (4-layer hierarchy) | Session model only (Phase 1) | Accept session model, log intent | +| Scribe fire-and-forget | Background, never read | Sync, must wait | Batch with last parallel group | +| Launch table UX | Show table → results later | Skip table → results with response | UX only — results are correct | +| SQL tool | Available | Not available | Avoid SQL in cross-platform code paths | +| Response order bug | Critical workaround | Possibly necessary (unverified) | Keep the block — harmless if unnecessary | + +#### SQL Tool Caveat + +The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitHub.com. Any coordinator logic or agent workflow that depends on SQL (todo tracking, batch processing, session state) will silently fail on non-CLI surfaces. Cross-platform code paths must not depend on SQL. Use filesystem-based state (`.squad/` files) for anything that must work everywhere. diff --git a/.squad/templates/constraint-tracking.md b/.squad/templates/constraint-tracking.md new file mode 100644 index 000000000..1936c3ff1 --- /dev/null +++ b/.squad/templates/constraint-tracking.md @@ -0,0 +1,38 @@ +# Constraint Budget Tracking + +When the user or system imposes constraints (question limits, revision limits, time budgets), maintain a visible counter in your responses and in the artifact. + +## Format + +``` +šŸ“Š Clarifying questions used: 2 / 3 +``` + +## Rules + +- Update the counter each time the constraint is consumed +- When a constraint is exhausted, state it: `šŸ“Š Question budget exhausted (3/3). Proceeding with current information.` +- If no constraints are active, do not display counters +- Include the final constraint status in multi-agent artifacts + +## Example Session + +``` +Coordinator: Spawning agents to analyze requirements... +šŸ“Š Clarifying questions used: 0 / 3 + +Agent asks clarification: "Should we support OAuth?" +Coordinator: Checking with user... +šŸ“Š Clarifying questions used: 1 / 3 + +Agent asks clarification: "What's the rate limit?" +Coordinator: Checking with user... +šŸ“Š Clarifying questions used: 2 / 3 + +Agent asks clarification: "Do we need RBAC?" +Coordinator: Checking with user... +šŸ“Š Clarifying questions used: 3 / 3 + +Agent asks clarification: "Should we cache responses?" +Coordinator: šŸ“Š Question budget exhausted (3/3). Proceeding without clarification. +``` diff --git a/.squad/templates/cooperative-rate-limiting.md b/.squad/templates/cooperative-rate-limiting.md new file mode 100644 index 000000000..bf56ef122 --- /dev/null +++ b/.squad/templates/cooperative-rate-limiting.md @@ -0,0 +1,229 @@ +# Cooperative Rate Limiting for Multi-Agent Deployments + +> Coordinate API quota across multiple Ralph instances to prevent cascading failures. + +## Problem + +The [circuit breaker template](ralph-circuit-breaker.md) handles single-instance rate limiting well. But when multiple Ralphs run across machines (or pods on K8s), each instance independently hits API limits: + +- **No coordination** — 5 Ralphs each think they have full API quota +- **Thundering herd** — All Ralphs retry simultaneously after rate limit resets +- **Priority inversion** — Low-priority work exhausts quota before critical work runs +- **Reactive only** — Circuit opens AFTER 429, wasting the failed request + +## Solution: 6-Pattern Architecture + +These patterns layer on top of the existing circuit breaker. Each is independent — adopt one or all. + +### Pattern 1: Traffic Light (RAAS — Rate-Aware Agent Scheduling) + +Map GitHub API `X-RateLimit-Remaining` to traffic light states: + +| State | Remaining % | Behavior | +|-------|------------|----------| +| 🟢 GREEN | >20% | Normal operation | +| 🟔 AMBER | 5–20% | Only P0 agents proceed | +| šŸ”“ RED | <5% | Block all except emergency P0 | + +```typescript +type TrafficLight = 'green' | 'amber' | 'red'; + +function getTrafficLight(remaining: number, limit: number): TrafficLight { + const pct = remaining / limit; + if (pct > 0.20) return 'green'; + if (pct > 0.05) return 'amber'; + return 'red'; +} + +function shouldProceed(light: TrafficLight, agentPriority: number): boolean { + if (light === 'green') return true; + if (light === 'amber') return agentPriority === 0; // P0 only + return false; // RED — block all +} +``` + +### Pattern 2: Cooperative Token Pool (CMARP) + +A shared JSON file (`~/.squad/rate-pool.json`) distributes API quota: + +```json +{ + "totalLimit": 5000, + "resetAt": "2026-03-22T20:00:00Z", + "allocations": { + "picard": { "priority": 0, "allocated": 2000, "used": 450, "leaseExpiry": "2026-03-22T19:55:00Z" }, + "data": { "priority": 1, "allocated": 1750, "used": 200, "leaseExpiry": "2026-03-22T19:55:00Z" }, + "ralph": { "priority": 2, "allocated": 1250, "used": 100, "leaseExpiry": "2026-03-22T19:55:00Z" } + } +} +``` + +**Rules:** +- P0 agents (Lead) get 40% of quota +- P1 agents (specialists) get 35% +- P2 agents (Ralph, Scribe) get 25% +- Stale leases (>5 minutes without heartbeat) are auto-recovered +- Each agent checks their remaining allocation before making API calls + +```typescript +interface RatePoolAllocation { + priority: number; + allocated: number; + used: number; + leaseExpiry: string; +} + +interface RatePool { + totalLimit: number; + resetAt: string; + allocations: Record; +} + +function canUseQuota(pool: RatePool, agentName: string): boolean { + const alloc = pool.allocations[agentName]; + if (!alloc) return true; // Unknown agent — allow (graceful) + + // Reclaim stale leases from crashed agents + const now = new Date(); + for (const [name, a] of Object.entries(pool.allocations)) { + if (new Date(a.leaseExpiry) < now && name !== agentName) { + a.allocated = 0; // Reclaim + } + } + + return alloc.used < alloc.allocated; +} +``` + +### Pattern 3: Predictive Circuit Breaker (PCB) + +Opens the circuit BEFORE getting a 429 by predicting when quota will run out: + +```typescript +interface RateSample { + timestamp: number; // Date.now() + remaining: number; // from X-RateLimit-Remaining header +} + +class PredictiveCircuitBreaker { + private samples: RateSample[] = []; + private readonly maxSamples = 10; + private readonly warningThresholdSeconds = 120; + + addSample(remaining: number): void { + this.samples.push({ timestamp: Date.now(), remaining }); + if (this.samples.length > this.maxSamples) { + this.samples.shift(); + } + } + + /** Predict seconds until quota exhaustion using linear regression */ + predictExhaustion(): number | null { + if (this.samples.length < 3) return null; + + const n = this.samples.length; + const first = this.samples[0]; + const last = this.samples[n - 1]; + + const elapsedMs = last.timestamp - first.timestamp; + if (elapsedMs === 0) return null; + + const consumedPerMs = (first.remaining - last.remaining) / elapsedMs; + if (consumedPerMs <= 0) return null; // Not consuming — safe + + const msUntilExhausted = last.remaining / consumedPerMs; + return msUntilExhausted / 1000; + } + + shouldOpen(): boolean { + const eta = this.predictExhaustion(); + if (eta === null) return false; + return eta < this.warningThresholdSeconds; + } +} +``` + +### Pattern 4: Priority Retry Windows (PWJG) + +Non-overlapping jitter windows prevent thundering herd: + +| Priority | Retry Window | Description | +|----------|-------------|-------------| +| P0 (Lead) | 500ms–5s | Recovers first | +| P1 (Specialists) | 2s–30s | Moderate delay | +| P2 (Ralph/Scribe) | 5s–60s | Most patient | + +```typescript +function getRetryDelay(priority: number, attempt: number): number { + const windows: Record = { + 0: [500, 5000], // P0: 500ms–5s + 1: [2000, 30000], // P1: 2s–30s + 2: [5000, 60000], // P2: 5s–60s + }; + + const [min, max] = windows[priority] ?? windows[2]; + const base = Math.min(min * Math.pow(2, attempt), max); + const jitter = Math.random() * base * 0.5; + return base + jitter; +} +``` + +### Pattern 5: Resource Epoch Tracker (RET) + +Heartbeat-based lease system for multi-machine deployments: + +```typescript +interface ResourceLease { + agent: string; + machine: string; + leaseStart: string; + leaseExpiry: string; // Typically 5 minutes from now + allocated: number; +} + +// Each agent renews its lease every 2 minutes +// If lease expires (agent crashed), allocation is reclaimed +``` + +### Pattern 6: Cascade Dependency Detector (CDD) + +Track downstream failures and apply backpressure: + +``` +Agent A (rate limited) → Agent B (waiting for A) → Agent C (waiting for B) + ↑ Backpressure signal: "don't start new work" +``` + +When a dependency is rate-limited, upstream agents should pause new work rather than queuing requests that will fail. + +## Kubernetes Integration + +On K8s, cooperative rate limiting can use KEDA to scale pods based on API quota: + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +spec: + scaleTargetRef: + name: ralph-deployment + triggers: + - type: external + metadata: + scalerAddress: keda-copilot-scaler:6000 + # Scaler returns 0 when rate limited → pods scale to zero +``` + +See [keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler) for a complete implementation. + +## Quick Start + +1. **Minimum viable:** Adopt Pattern 1 (Traffic Light) — read `X-RateLimit-Remaining` from API responses +2. **Multi-machine:** Add Pattern 2 (Cooperative Pool) — shared `rate-pool.json` +3. **Production:** Add Pattern 3 (Predictive CB) — prevent 429s entirely +4. **Kubernetes:** Add KEDA scaler for automatic pod scaling + +## References + +- [Circuit Breaker Template](ralph-circuit-breaker.md) — Foundation patterns +- [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Production K8s deployment +- [KEDA Copilot Scaler](https://github.com/tamirdresher/keda-copilot-scaler) — Custom KEDA external scaler diff --git a/.squad/templates/copilot-agent.md b/.squad/templates/copilot-agent.md new file mode 100644 index 000000000..e8ae4612c --- /dev/null +++ b/.squad/templates/copilot-agent.md @@ -0,0 +1,96 @@ +# Copilot Coding Agent Member + +On-demand reference for adding the GitHub Copilot coding agent (@copilot) to the Squad roster. + +## Adding @copilot + +When the user says "add copilot", "add the coding agent", or "use @copilot for issues": + +1. **Add to team.md roster:** + ```markdown + | @copilot | Coding Agent | — | šŸ¤– Coding Agent | + ``` +2. **Add capability profile** (below the roster table): + ```markdown + + ### @copilot — Capability Profile + + | Capability | Level | Notes | + |-----------|-------|-------| + | Bug fixes (well-scoped) | 🟢 | Best for isolated, test-covered fixes | + | Feature implementation | 🟔 | Works well with clear specs; may need review | + | Refactoring | 🟔 | Handles mechanical refactors; verify scope | + | Architecture decisions | šŸ”“ | Cannot make cross-cutting design choices | + | Multi-repo coordination | šŸ”“ | Limited to single-repo context | + | Test writing | 🟢 | Strong at adding tests for existing code | + | Documentation | 🟢 | Generates docs from code effectively | + ``` +3. **Add routing entries** to routing.md for appropriate work types. +4. **Do not create** `charter.md` — @copilot uses `copilot-instructions.md` instead. + +## Comparison: Spawned Agent vs. @copilot + +| | Spawned Agent | @copilot | +|---|--------------|----------| +| Execution model | Sync sub-task within session | Async — picks up assigned issues | +| Branch convention | `squad/{issue}-{slug}` | `copilot/{slug}` | +| Trigger | Coordinator spawns directly | Issue assignment | +| Charter source | `.squad/agents/{name}/charter.md` | `.github/copilot-instructions.md` | +| Context window | Inherits full session context | Fresh context per issue | +| Reviewer gating | āœ… Enforced by coordinator | āœ… Via PR review process | +| Speed | Immediate (in-session) | Minutes (async queue) | + +## Roster Format + +In `team.md`, @copilot always appears as: + +```markdown +| @copilot | Coding Agent | — | šŸ¤– Coding Agent | +``` + +- **No casting** — always "@copilot" (literal handle). +- **No charter file** — configuration lives in `.github/copilot-instructions.md`. +- **No history file** — work is tracked via PRs and issue comments. + +## Auto-Assign Behavior + +Controlled by the HTML comment in team.md: + +```markdown + +``` + +| Setting | Behavior | +|---------|----------| +| `true` | Lead assigns routed issues to @copilot automatically via `gh issue edit --add-assignee @copilot` | +| `false` | Lead presents recommendation; user confirms before assignment | + +## Lead Triage Integration + +During triage, Lead evaluates each issue against @copilot's capability profile: + +1. **🟢 Match** — Auto-assign (if enabled) or recommend assignment. +2. **🟔 Match** — Assign with note: "āš ļø May need review — @copilot is 🟔 for this type of work." +3. **šŸ”“ Match** — Skip @copilot; route to appropriate spawned agent or human. + +## Routing Details + +Add to `routing.md`: + +```markdown +| bug fixes (isolated, test-covered) | @copilot šŸ¤– | Single-file fixes, test additions | +| documentation updates | @copilot šŸ¤– | README, API docs, inline comments | +| test coverage gaps | @copilot šŸ¤– | Adding missing test cases | +``` + +Work that routes to @copilot: +- Creates/assigns the GitHub issue (if not already) +- Does NOT spawn a sub-agent — @copilot works asynchronously +- Coordinator reports: "šŸ¤– Assigned #{number} to @copilot — will open a PR when ready." +- Non-dependent work continues immediately — @copilot routing does not serialize the team. + +## Monitoring @copilot Work + +On each watch cycle (or when user asks "status"): +- Check for open PRs from `copilot/*` branches. +- Report: "šŸ¤– @copilot: {N} PRs open ({list}). {M} issues assigned, pending." diff --git a/.squad/templates/copilot-instructions.md b/.squad/templates/copilot-instructions.md new file mode 100644 index 000000000..2ba998c8e --- /dev/null +++ b/.squad/templates/copilot-instructions.md @@ -0,0 +1,60 @@ +# Copilot Coding Agent — Squad Instructions + +You are working on a project that uses **Squad**, an AI team framework. When picking up issues autonomously, follow these guidelines. + +## Coordinator Canary Check + +**IMPORTANT — run this check on EVERY session start, before doing any work:** + +Look for the exact token `SQUAD_COORDINATOR_CANARY_a8f3` in your `agent_instructions`. This token is placed at the end of `squad.agent.md`. If it is missing, the coordinator file is missing or truncated. + +**When the token is missing, you MUST:** +1. **STOP** — do not proceed with standard Squad behavior. +2. **WARN the user** with this exact message: + ``` + āš ļø Squad coordinator (squad.agent.md) appears to be missing or truncated. The canary token was not found. Do NOT proceed with standard Squad behavior — Squad's safety rails are not loaded. Please restart your session. + ``` +3. Do not continue with normal Squad routing, spawning, PR, or branch-protection behavior after emitting the warning. + +## Team Context + +Before starting work on any issue: + +1. Read `.squad/team.md` for the team roster, member roles, and your capability profile. +2. Read `.squad/routing.md` for work routing rules. +3. If the issue has a `squad:{member}` label, read that member's charter at `.squad/agents/{member}/charter.md` to understand their domain expertise and coding style — work in their voice. + +## Capability Self-Check + +Before starting work, check your capability profile in `.squad/team.md` under the **Coding Agent → Capabilities** section. + +- **🟢 Good fit** — proceed autonomously. +- **🟔 Needs review** — proceed, but note in the PR description that a squad member should review. +- **šŸ”“ Not suitable** — do NOT start work. Instead, comment on the issue: + ``` + šŸ¤– This issue doesn't match my capability profile (reason: {why}). Suggesting reassignment to a squad member. + ``` + +## Branch Naming + +Use the squad branch convention: +``` +squad/{issue-number}-{kebab-case-slug} +``` +Example: `squad/42-fix-login-validation` + +## PR Guidelines + +When opening a PR: +- Reference the issue: `Closes #{issue-number}` +- If the issue had a `squad:{member}` label, mention the member: `Working as {member} ({role})` +- If this is a 🟔 needs-review task, add to the PR description: `āš ļø This task was flagged as "needs review" — please have a squad member review before merging.` +- Follow any project conventions in `.squad/decisions.md` + +## Decisions + +If you make a decision that affects other team members, write it to: +``` +.squad/decisions/inbox/copilot-{brief-slug}.md +``` +The Scribe will merge it into the shared decisions file. diff --git a/.squad/templates/fact-checker-charter.md b/.squad/templates/fact-checker-charter.md new file mode 100644 index 000000000..1d03e0b4e --- /dev/null +++ b/.squad/templates/fact-checker-charter.md @@ -0,0 +1,83 @@ +# Fact Checker + +> Trust, but verify. Every claim gets a source check. + +## Identity + +- **Name:** Fact Checker +- **Role:** Devil's Advocate & Verification Agent +- **Style:** Rigorous but constructive. Flags issues clearly without being abrasive. +- **Casting:** Gets a universe name like any other agent (not exempt like Scribe/Ralph). + +## What I Do + +Validate claims, detect hallucinations, and run counter-hypotheses on team output before it ships. + +## Verification Methodology + +For every claim or assertion I review: + +1. **Source Check:** What evidence supports this? Can I verify it? +2. **Counter-Hypothesis:** What would disprove this? Is there an alternative explanation? +3. **Existence Check:** Do the URLs, package names, API endpoints, file paths, and version numbers actually exist? +4. **Consistency Check:** Does this contradict anything in `.squad/decisions.md` or prior team output? + +## Confidence Ratings + +Every verified item gets one of: + +| Rating | Meaning | +|--------|---------| +| āœ… Verified | Confirmed via source, test, or direct observation | +| āš ļø Unverified | Plausible but could not confirm — needs human review | +| āŒ Contradicted | Found evidence that contradicts the claim | +| šŸ” Needs Investigation | Requires deeper analysis beyond current scope | + +## When I'm Triggered + +- **Auto-trigger (via routing):** Tasks tagged with `review`, `verify`, `fact-check`, `audit` +- **Pre-publish gate:** Before any artifact is delivered to the user, if configured +- **Manual:** User says "fact-check this", "verify these claims", "double-check" +- **Post-research:** After any agent produces research output or external references + +## How I Work + +1. **Read the artifact** — understand what's being claimed +2. **Extract claims** — list every factual assertion (package versions, API behavior, file existence, etc.) +3. **Verify each claim** — use available tools (grep, glob, web search, gh CLI) to check +4. **Run counter-hypotheses** — for key assumptions, ask "what if this is wrong?" +5. **Produce a verification report:** + +```markdown +## Verification Report — {artifact name} + +### Claims Verified +- āœ… {claim} — confirmed via {source} +- āš ļø {claim} — could not verify, {reason} +- āŒ {claim} — contradicted by {evidence} + +### Counter-Hypotheses +- {assumption} → Alternative: {counter} + +### Recommendation +{proceed / revise / block with reasons} +``` + +6. **Write decision** if I found issues: `.squad/decisions/inbox/fact-checker-{slug}.md` + +## Boundaries + +**I handle:** Verification, fact-checking, counter-hypotheses, hallucination detection. + +**I don't handle:** Implementation, design, testing, or docs. I review, not create. + +**I am not a blocker by default.** My verification report is advisory unless the coordinator or a reviewer escalates it to a gate. + +## Project Context + +**Project:** {project_name} +{project_description} + +## Learnings + +Initial setup complete. Ready for verification work. diff --git a/.squad/templates/history.md b/.squad/templates/history.md new file mode 100644 index 000000000..d975a5cbf --- /dev/null +++ b/.squad/templates/history.md @@ -0,0 +1,10 @@ +# Project Context + +- **Owner:** {user name} +- **Project:** {project description} +- **Stack:** {languages, frameworks, tools} +- **Created:** {timestamp} + +## Learnings + + diff --git a/.squad/templates/identity/now.md b/.squad/templates/identity/now.md new file mode 100644 index 000000000..04e1dfeeb --- /dev/null +++ b/.squad/templates/identity/now.md @@ -0,0 +1,9 @@ +--- +updated_at: {timestamp} +focus_area: {brief description} +active_issues: [] +--- + +# What We're Focused On + +{Narrative description of current focus — 1-3 sentences. Updated by coordinator at session start.} diff --git a/.squad/templates/identity/wisdom.md b/.squad/templates/identity/wisdom.md new file mode 100644 index 000000000..c3b978e4f --- /dev/null +++ b/.squad/templates/identity/wisdom.md @@ -0,0 +1,15 @@ +--- +last_updated: {timestamp} +--- + +# Team Wisdom + +Reusable patterns and heuristics learned through work. NOT transcripts — each entry is a distilled, actionable insight. + +## Patterns + + + +## Anti-Patterns + + diff --git a/.squad/templates/issue-lifecycle.md b/.squad/templates/issue-lifecycle.md new file mode 100644 index 000000000..aea93654e --- /dev/null +++ b/.squad/templates/issue-lifecycle.md @@ -0,0 +1,413 @@ +# Issue Lifecycle — Repo Connection & PR Flow + +Reference for connecting Squad to a repository and managing the issue→branch→PR→merge lifecycle. + +## Repo Connection Format + +When connecting Squad to an issue tracker, store the connection in `.squad/team.md`: + +```markdown +## Issue Source + +**Repository:** {owner}/{repo} +**Connected:** {date} +**Platform:** {GitHub | Azure DevOps | Planner} +**Filters:** +- Labels: `{label-filter}` +- Project: `{project-name}` (ADO/Planner only) +- Plan: `{plan-id}` (Planner only) +``` + +**Detection triggers:** +- User says "connect to {repo}" +- User says "monitor {repo} for issues" +- Ralph is activated without an issue source + +## Platform-Specific Issue States + +Each platform tracks issue lifecycle differently. Squad normalizes these into a common board state. + +### GitHub + +| GitHub State | GitHub API Fields | Squad Board State | +|--------------|-------------------|-------------------| +| Open, no assignee | `state: open`, `assignee: null` | `untriaged` | +| Open, assigned, no branch | `state: open`, `assignee: @user`, no linked PR | `assigned` | +| Open, branch exists | `state: open`, linked branch exists | `inProgress` | +| Open, PR opened | `state: open`, PR exists, `reviewDecision: null` | `needsReview` | +| Open, PR approved | `state: open`, PR `reviewDecision: APPROVED` | `readyToMerge` | +| Open, changes requested | `state: open`, PR `reviewDecision: CHANGES_REQUESTED` | `changesRequested` | +| Open, CI failure | `state: open`, PR `statusCheckRollup: FAILURE` | `ciFailure` | +| Closed | `state: closed` | `done` | + +**Issue labels used by Squad:** +- `squad` — Issue is in Squad backlog +- `squad:{member}` — Assigned to specific agent +- `squad:untriaged` — Needs triage +- `go:needs-research` — Needs investigation before implementation +- `priority:p{N}` — Priority level (0=critical, 1=high, 2=medium, 3=low) +- `next-up` — Queued for next agent pickup + +**Branch naming convention:** +``` +squad/{issue-number}-{kebab-case-slug} +``` +Example: `squad/42-fix-login-validation` + +### Azure DevOps + +| ADO State | Squad Board State | +|-----------|-------------------| +| New | `untriaged` | +| Active, no branch | `assigned` | +| Active, branch exists | `inProgress` | +| Active, PR opened | `needsReview` | +| Active, PR approved | `readyToMerge` | +| Resolved | `done` | +| Closed | `done` | + +**Work item tags used by Squad:** +- `squad` — Work item is in Squad backlog +- `squad:{member}` — Assigned to specific agent + +**Branch naming convention:** +``` +squad/{work-item-id}-{kebab-case-slug} +``` +Example: `squad/1234-add-auth-module` + +### Microsoft Planner + +Planner does not have native Git integration. Squad uses Planner for task tracking and GitHub/ADO for code management. + +| Planner Status | Squad Board State | +|----------------|-------------------| +| Not Started | `untriaged` | +| In Progress, no PR | `inProgress` | +| In Progress, PR opened | `needsReview` | +| Completed | `done` | + +**Planner→Git workflow:** +1. Task created in Planner bucket +2. Agent reads task from Planner +3. Agent creates branch in GitHub/ADO repo +4. Agent opens PR referencing Planner task ID in description +5. Agent marks task as "Completed" when PR merges + +## Issue → Branch → PR → Merge Lifecycle + +### 1. Issue Assignment (Triage) + +**Trigger:** Ralph detects an untriaged issue or user manually assigns work. + +**Actions:** +1. Read `.squad/routing.md` to determine which agent should handle the issue +2. Apply `squad:{member}` label (GitHub) or tag (ADO) +3. Transition issue to `assigned` state +4. Optionally spawn agent immediately if issue is high-priority + +**Issue read command:** +```bash +# GitHub +gh issue view {number} --json number,title,body,labels,assignees + +# Azure DevOps +az boards work-item show --id {id} --output json +``` + +### 2. Branch Creation (Start Work) + +**Trigger:** Agent accepts issue assignment and begins work. + +**Actions:** +1. Ensure working on latest base branch (usually `main` or `dev`) +2. Create feature branch using Squad naming convention +3. Transition issue to `inProgress` state + +**Branch creation commands:** + +**Standard (single-agent, no parallelism):** +```bash +git checkout main && git pull && git checkout -b squad/{issue-number}-{slug} +``` + +**Worktree (parallel multi-agent):** +```bash +git worktree add ../worktrees/{issue-number} -b squad/{issue-number}-{slug} +cd ../worktrees/{issue-number} +``` + +> **Note:** Worktree support is in progress (#525). Current implementation uses standard checkout. + +### 3. Implementation & Commit + +**Actions:** +1. Agent makes code changes +2. Commits reference the issue number +3. Pushes branch to remote + +**Commit message format:** +``` +{type}({scope}): {description} (#{issue-number}) + +{detailed explanation if needed} + +{breaking change notice if applicable} + +Closes #{issue-number} + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +``` + +**Commit types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `style`, `build`, `ci` + +**Push command:** +```bash +git push -u origin squad/{issue-number}-{slug} +``` + +### 4. PR Creation + +**Trigger:** Agent completes implementation and is ready for review. + +**Actions:** +1. Open PR from feature branch to base branch +2. Reference issue in PR description +3. Apply labels if needed +4. Transition issue to `needsReview` state + +**PR creation commands:** + +**GitHub:** +```bash +gh pr create --title "{title}" \ + --body "Closes #{issue-number}\n\n{description}" \ + --head squad/{issue-number}-{slug} \ + --base main +``` + +**Azure DevOps:** +```bash +az repos pr create --title "{title}" \ + --description "Closes #{work-item-id}\n\n{description}" \ + --source-branch squad/{work-item-id}-{slug} \ + --target-branch main +``` + +**PR description template:** +```markdown +Closes #{issue-number} + +## Summary +{what changed} + +## Changes +- {change 1} +- {change 2} + +## Testing +{how this was tested} + +{If working as a squad member:} +Working as {member} ({role}) + +{If needs human review:} +āš ļø This task was flagged as "needs review" — please have a squad member review before merging. +``` + +### 5. PR Review & Updates + +**Review states:** +- **Approved** → `readyToMerge` +- **Changes requested** → `changesRequested` +- **CI failure** → `ciFailure` + +**When changes are requested:** +1. Agent addresses feedback +2. Commits fixes to the same branch +3. Pushes updates +4. Requests re-review + +**Update workflow:** +```bash +# Make changes +# āš ļø NEVER use `git add .` or `git add -A` — only stage files you intentionally changed +git add -- {specific files you modified} +git commit -m "fix: address review feedback" +git push +``` + +**Re-request review (GitHub):** +```bash +gh pr ready {pr-number} +``` + +### 6. PR Merge + +**Trigger:** PR is approved and CI passes. + +**Merge strategies:** + +**GitHub (merge commit):** +```bash +gh pr merge {pr-number} --merge --delete-branch +``` + +**GitHub (squash):** +```bash +gh pr merge {pr-number} --squash --delete-branch +``` + +**Azure DevOps:** +```bash +az repos pr update --id {pr-id} --status completed --delete-source-branch true +``` + +**Post-merge actions:** +1. Issue automatically closes (if "Closes #{number}" is in PR description) +2. Feature branch is deleted +3. Squad board state transitions to `done` +4. Worktree cleanup (if worktree was used — #525) + +### 7. Cleanup + +**Standard workflow cleanup:** +```bash +git checkout main +git pull +git branch -d squad/{issue-number}-{slug} +``` + +**Worktree cleanup (future, #525):** +```bash +cd {original-cwd} +git worktree remove ../worktrees/{issue-number} +``` + +## Spawn Prompt Additions for Issue Work + +When spawning an agent to work on an issue, include this context block: + +```markdown +## ISSUE CONTEXT + +**Issue:** #{number} — {title} +**Platform:** {GitHub | Azure DevOps | Planner} +**Repository:** {owner}/{repo} +**Assigned to:** {member} + +**Description:** +{issue body} + +**Labels/Tags:** +{labels} + +**Acceptance Criteria:** +{criteria if present in issue} + +**Branch:** `squad/{issue-number}-{slug}` + +**Your task:** +{specific directive to the agent} + +**After completing work:** +1. Commit with message referencing issue number +2. Push branch +3. Open PR using: + ``` + gh pr create --title "{title}" --body "Closes #{number}\n\n{description}" --head squad/{issue-number}-{slug} --base {base-branch} + ``` +4. Report PR URL to coordinator +``` + +## Ralph's Role in Issue Lifecycle + +Ralph (the work monitor) continuously checks issue and PR state: + +1. **Triage:** Detects untriaged issues, assigns `squad:{member}` labels +2. **Spawn:** Launches agents for assigned issues +3. **Monitor:** Tracks PR state transitions (needsReview → changesRequested → readyToMerge) +4. **Merge:** Automatically merges approved PRs +5. **Cleanup:** Marks issues as done when PRs merge + +**Ralph's work-check cycle:** +``` +Scan → Categorize → Dispatch → Watch → Report → Loop +``` + +See `.squad/templates/ralph-reference.md` for Ralph's full lifecycle. + +## PR Review Handling + +### Automated Approval (CI-only projects) + +If the project has no human reviewers configured: +1. PR opens +2. CI runs +3. If CI passes, Ralph auto-merges +4. Issue closes + +### Human Review Required + +If the project requires human approval: +1. PR opens +2. Human reviewer is notified (GitHub/ADO notifications) +3. Reviewer approves or requests changes +4. If approved + CI passes, Ralph merges +5. If changes requested, agent addresses feedback + +### Squad Member Review + +If the issue was assigned to a squad member and they authored the PR: +1. Another squad member reviews (conflict of interest avoidance) +2. Original author is locked out from re-working rejected code (rejection lockout) +3. Reviewer can approve edits or reject outright + +## Common Issue Lifecycle Patterns + +### Pattern 1: Quick Fix (Single Agent, No Review) +``` +Issue created → Assigned to agent → Branch created → Code fixed → +PR opened → CI passes → Auto-merged → Issue closed +``` + +### Pattern 2: Feature Development (Human Review) +``` +Issue created → Assigned to agent → Branch created → Feature implemented → +PR opened → Human reviews → Changes requested → Agent fixes → +Re-reviewed → Approved → Merged → Issue closed +``` + +### Pattern 3: Research-Then-Implement +``` +Issue created → Labeled `go:needs-research` → Research agent spawned → +Research documented → Research PR merged → Implementation issue created → +Implementation agent spawned → Feature built → PR merged +``` + +### Pattern 4: Parallel Multi-Agent (Future, #525) +``` +Epic issue created → Decomposed into sub-issues → Each sub-issue assigned → +Multiple agents work in parallel worktrees → PRs opened concurrently → +All PRs reviewed → All PRs merged → Epic closed +``` + +## Anti-Patterns + +- āŒ Creating branches without linking to an issue +- āŒ Committing without issue reference in message +- āŒ Opening PRs without "Closes #{number}" in description +- āŒ Merging PRs before CI passes +- āŒ Leaving feature branches undeleted after merge +- āŒ Using `checkout -b` when parallel agents are active (causes working directory conflicts) +- āŒ Manually transitioning issue states — let the platform and Squad automation handle it +- āŒ Skipping the branch naming convention — breaks Ralph's tracking logic + +## Migration Notes + +**v0.8.x → v0.9.x (Worktree Support):** +- `checkout -b` → `git worktree add` for parallel agents +- Worktree cleanup added to post-merge flow +- `TEAM_ROOT` passing to agents to support worktree-aware state resolution + +This template will be updated as worktree lifecycle support lands in #525. diff --git a/.squad/templates/keda-scaler.md b/.squad/templates/keda-scaler.md new file mode 100644 index 000000000..ba1646c5f --- /dev/null +++ b/.squad/templates/keda-scaler.md @@ -0,0 +1,164 @@ +# KEDA External Scaler for GitHub Issue-Driven Agent Autoscaling + +> Scale agent pods to zero when idle, up when work arrives — driven by GitHub Issues. + +## Overview + +When running Squad on Kubernetes, agent pods sit idle when no work exists. [KEDA](https://keda.sh) (Kubernetes Event-Driven Autoscaler) solves this for queue-based workloads, but GitHub Issues isn't a native KEDA trigger. + +The `keda-copilot-scaler` is a KEDA External Scaler (gRPC) that bridges this gap: +1. Polls GitHub API for issues matching specific labels (e.g., `squad:copilot`) +2. Reports queue depth as a KEDA metric +3. Handles rate limits gracefully (Retry-After, exponential backoff) +4. Supports composite scaling decisions + +## Quick Start + +### Prerequisites +- Kubernetes cluster with KEDA v2.x installed +- GitHub personal access token (PAT) with `repo` scope +- Helm 3.x + +### 1. Install the Scaler + +```bash +helm install keda-copilot-scaler oci://ghcr.io/tamirdresher/keda-copilot-scaler \ + --namespace squad-scaler --create-namespace \ + --set github.owner=YOUR_ORG \ + --set github.repo=YOUR_REPO \ + --set github.token=YOUR_TOKEN +``` + +Or with Kustomize: +```bash +kubectl apply -k https://github.com/tamirdresher/keda-copilot-scaler/deploy/kustomize +``` + +### 2. Create a ScaledObject + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: picard-scaler + namespace: squad +spec: + scaleTargetRef: + name: picard-deployment + minReplicaCount: 0 # Scale to zero when idle + maxReplicaCount: 3 + pollingInterval: 30 # Check every 30 seconds + cooldownPeriod: 300 # Wait 5 minutes before scaling down + triggers: + - type: external + metadata: + scalerAddress: keda-copilot-scaler.squad-scaler.svc.cluster.local:6000 + owner: your-org + repo: your-repo + labels: squad:copilot # Only count issues with this label + threshold: "1" # Scale up when >= 1 issue exists +``` + +### 3. Verify + +```bash +# Check the scaler is running +kubectl get pods -n squad-scaler + +# Check ScaledObject status +kubectl get scaledobject picard-scaler -n squad + +# Watch scaling events +kubectl get events -n squad --watch +``` + +## Scaling Behavior + +| Open Issues | Target Replicas | Behavior | +|------------|----------------|----------| +| 0 | 0 | Scale to zero — save resources | +| 1–3 | 1 | Single agent handles work | +| 4–10 | 2 | Scale up for parallel processing | +| 10+ | 3 (max) | Maximum parallelism | + +The threshold and max replicas are configurable per ScaledObject. + +## Rate Limit Awareness + +The scaler tracks GitHub API rate limits: +- Reads `X-RateLimit-Remaining` from API responses +- Backs off when quota is low (< 100 remaining) +- Reports rate limit metrics as secondary KEDA triggers +- Never exhausts API quota from polling + +## Integration with Squad + +### Machine Capabilities (#514) + +Combine with machine capability labels for intelligent scheduling: + +```yaml +# Only scale pods on GPU-capable nodes +spec: + template: + spec: + nodeSelector: + node.squad.dev/gpu: "true" + triggers: + - type: external + metadata: + labels: squad:copilot,needs:gpu +``` + +### Cooperative Rate Limiting (#515) + +The scaler exposes rate limit metrics that feed into the cooperative rate limiting system: +- Current `X-RateLimit-Remaining` value +- Predicted time to exhaustion (from predictive circuit breaker) +- Can return 0 target replicas when rate limited → pods scale to zero + +## Architecture + +``` +GitHub API KEDA Kubernetes +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Issues │◄── poll ──►│ Scaler │──metrics─►│ HPA / KEDA │ +│ (REST) │ │ (gRPC) │ │ Controller │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + scale up/down + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ Agent Pods │ + │ (0–N replicas)│ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## Configuration Reference + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `github.owner` | — | Repository owner | +| `github.repo` | — | Repository name | +| `github.token` | — | GitHub PAT with `repo` scope | +| `github.labels` | `squad:copilot` | Comma-separated label filter | +| `scaler.port` | `6000` | gRPC server port | +| `scaler.pollInterval` | `30s` | GitHub API polling interval | +| `scaler.rateLimitThreshold` | `100` | Stop polling below this remaining | + +## Source & Contributing + +- **Repository:** [tamirdresher/keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler) +- **License:** MIT +- **Language:** Go +- **Tests:** 51 passing (unit + integration) +- **CI:** GitHub Actions + +The scaler is maintained as a standalone project. PRs and issues welcome. + +## References + +- [KEDA External Scalers](https://keda.sh/docs/latest/concepts/external-scalers/) — KEDA documentation +- [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Full Kubernetes deployment example +- [Machine Capabilities](machine-capabilities.md) — Capability-based routing (#514) +- [Cooperative Rate Limiting](cooperative-rate-limiting.md) — Multi-agent rate management (#515) diff --git a/.squad/templates/loop.md b/.squad/templates/loop.md new file mode 100644 index 000000000..5a6fc5904 --- /dev/null +++ b/.squad/templates/loop.md @@ -0,0 +1,46 @@ +--- +configured: false +interval: 10 +timeout: 30 +description: "My squad work loop" +--- + +# Squad Work Loop + +> āš ļø Set `configured: true` in the frontmatter above to activate this loop. +> Run with: `squad loop` + +## What to do each cycle + +Describe what your squad should do every time the loop wakes up. Be specific — +the more context you give, the better your squad performs. + +Examples: +- Check for new messages in a Teams channel and summarize action items +- Review recent pull requests and flag anything needing attention +- Run a health check on staging and report anomalies +- Scan the inbox for anything that needs a response today + + + +## Monitoring (optional) + +If you want your squad to watch external channels, enable monitor capabilities: + +```bash +squad loop --monitor-email --monitor-teams +``` + +## Personality (optional) + +If your squad has a specific voice or style, describe it here so each cycle +stays consistent. + +Example: "Be concise. Use bullet points. Flag blockers clearly." + +## Tips + +- **Be specific.** Vague prompts produce vague results. +- **Set boundaries.** Tell the squad what NOT to do (e.g., "Don't send messages to anyone but me"). +- **Start small.** Begin with one task per cycle, then expand. +- **Use frontmatter.** `interval` controls how often the loop runs. `timeout` caps each cycle. diff --git a/.squad/templates/machine-capabilities.md b/.squad/templates/machine-capabilities.md new file mode 100644 index 000000000..b770fd04b --- /dev/null +++ b/.squad/templates/machine-capabilities.md @@ -0,0 +1,75 @@ +# Machine Capability Discovery & Label-Based Routing + +> Enable Ralph to skip issues requiring capabilities the current machine lacks. + +## Overview + +When running Squad across multiple machines (laptops, DevBoxes, GPU servers, Kubernetes nodes), each machine has different tooling. The capability system lets you declare what each machine can do, and Ralph automatically routes work accordingly. + +## Setup + +### 1. Create a Capabilities Manifest + +Create `~/.squad/machine-capabilities.json` (user-wide) or `.squad/machine-capabilities.json` (project-local): + +```json +{ + "machine": "MY-LAPTOP", + "capabilities": ["browser", "personal-gh", "onedrive"], + "missing": ["gpu", "docker", "azure-speech"], + "lastUpdated": "2026-03-22T00:00:00Z" +} +``` + +### 2. Label Issues with Requirements + +Add `needs:*` labels to issues that require specific capabilities: + +| Label | Meaning | +|-------|---------| +| `needs:browser` | Requires Playwright / browser automation | +| `needs:gpu` | Requires NVIDIA GPU | +| `needs:personal-gh` | Requires personal GitHub account | +| `needs:emu-gh` | Requires Enterprise Managed User account | +| `needs:azure-cli` | Requires authenticated Azure CLI | +| `needs:docker` | Requires Docker daemon | +| `needs:onedrive` | Requires OneDrive sync | +| `needs:teams-mcp` | Requires Teams MCP tools | + +Custom capabilities are supported — any `needs:X` label works if `X` is in the machine's `capabilities` array. + +### 3. Run Ralph + +```bash +squad watch --interval 5 +``` + +Ralph will log skipped issues: +``` +ā­ļø Skipping #42 "Train ML model" — missing: gpu +āœ“ Triaged #43 "Fix CSS layout" → Picard (routing-rule) +``` + +## How It Works + +1. Ralph loads `machine-capabilities.json` at startup +2. For each open issue, Ralph extracts `needs:*` labels +3. If any required capability is missing, the issue is skipped +4. Issues without `needs:*` labels are always processed (opt-in system) + +## Kubernetes Integration + +On Kubernetes, machine capabilities map to node labels: + +```yaml +# Node labels (set by capability DaemonSet or manually) +node.squad.dev/gpu: "true" +node.squad.dev/browser: "true" + +# Pod spec uses nodeSelector +spec: + nodeSelector: + node.squad.dev/gpu: "true" +``` + +A DaemonSet can run capability discovery on each node and maintain labels automatically. See the [squad-on-aks](https://github.com/tamirdresher/squad-on-aks) project for a complete Kubernetes deployment example. \ No newline at end of file diff --git a/.squad/templates/mcp-config.md b/.squad/templates/mcp-config.md new file mode 100644 index 000000000..f38425e4c --- /dev/null +++ b/.squad/templates/mcp-config.md @@ -0,0 +1,88 @@ +# MCP Integration — Configuration and Samples + +MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. + +## Config File Locations + +Users configure MCP servers at these locations (checked in priority order): +1. **Repository-level:** `.copilot/mcp-config.json` (team-shared, committed to repo) +2. **Workspace-level:** `.vscode/mcp.json` (VS Code workspaces) +3. **User-level:** `~/.copilot/mcp-config.json` (personal) +4. **CLI override:** `--additional-mcp-config` flag (session-specific) + +## Sample Config — Trello + +```json +{ + "mcpServers": { + "trello": { + "command": "npx", + "args": ["-y", "@trello/mcp-server"], + "env": { + "TRELLO_API_KEY": "${TRELLO_API_KEY}", + "TRELLO_TOKEN": "${TRELLO_TOKEN}" + } + } + } +} +``` + +## Sample Config — GitHub + +```json +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_TOKEN": "${GITHUB_TOKEN}" + } + } + } +} +``` + +## Sample Config — Azure + +```json +{ + "mcpServers": { + "azure": { + "command": "npx", + "args": ["-y", "@azure/mcp-server"], + "env": { + "AZURE_SUBSCRIPTION_ID": "${AZURE_SUBSCRIPTION_ID}", + "AZURE_CLIENT_ID": "${AZURE_CLIENT_ID}", + "AZURE_CLIENT_SECRET": "${AZURE_CLIENT_SECRET}", + "AZURE_TENANT_ID": "${AZURE_TENANT_ID}" + } + } + } +} +``` + +## Sample Config — Aspire + +```json +{ + "mcpServers": { + "aspire": { + "command": "npx", + "args": ["-y", "@aspire/mcp-server"], + "env": { + "ASPIRE_DASHBOARD_URL": "${ASPIRE_DASHBOARD_URL}" + } + } + } +} +``` + +## Authentication Notes + +- **GitHub MCP requires a separate token** from the `gh` CLI auth. Generate at https://github.com/settings/tokens +- **Trello requires API key + token** from https://trello.com/power-ups/admin +- **Azure requires service principal credentials** — see Azure docs for setup +- **Aspire uses the dashboard URL** — typically `http://localhost:18888` during local dev + +Auth is a real blocker for some MCP servers. Users need separate tokens for GitHub MCP, Azure MCP, Trello MCP, etc. This is a documentation problem, not a code problem. diff --git a/.squad/templates/model-selection-reference.md b/.squad/templates/model-selection-reference.md new file mode 100644 index 000000000..2421f537c --- /dev/null +++ b/.squad/templates/model-selection-reference.md @@ -0,0 +1,101 @@ +# Model Selection Reference + +### Per-Agent Model Selection + +Before spawning an agent, determine which model to use. Check these layers in order — first match wins: + +**Layer 0 — Persistent Config (`.squad/config.json`):** On session start, read `.squad/config.json`. If `agentModelOverrides.{agentName}` exists, use that model for this specific agent. Otherwise, if `defaultModel` exists, use it for ALL agents. This layer survives across sessions — the user set it once and it sticks. + +- **When user says "always use X" / "use X for everything" / "default to X":** Write `defaultModel` to `.squad/config.json`. Acknowledge: `āœ… Model preference saved: {model} — all future sessions will use this until changed.` +- **When user says "use X for {agent}":** Write to `agentModelOverrides.{agent}` in `.squad/config.json`. Acknowledge: `āœ… {Agent} will always use {model} — saved to config.` +- **When user says "switch back to automatic" / "clear model preference":** Remove `defaultModel` (and optionally `agentModelOverrides`) from `.squad/config.json`. Acknowledge: `āœ… Model preference cleared — returning to automatic selection.` + +**Layer 1 — Session Directive:** Did the user specify a model for this session? ("use opus for this session", "save costs"). If yes, use that model. Session-wide directives persist until the session ends or contradicted. + +**Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. + +**Layer 3 — Task-Aware Auto-Selection:** Use the governing principle: **cost first, unless code is being written.** Match the agent's task to determine output type, then select accordingly: + +| Task Output | Model | Tier | Rule | +|-------------|-------|------|------| +| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.6` | Standard | Quality and accuracy matter for code. Use standard tier. | +| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.6` | Standard | Prompts are executable — treat like code. | +| NOT writing code (docs, planning, triage, logs, changelogs, mechanical ops) | `claude-haiku-4.5` | Fast | Cost first. Haiku handles non-code tasks. | +| Visual/design work requiring image analysis | `claude-opus-4.5` | Premium | Vision capability required. Overrides cost rule. | + +**Role-to-model mapping** (applying cost-first principle): + +| Role | Default Model | Why | Override When | +|------|--------------|-----|---------------| +| Core Dev / Backend / Frontend | `claude-sonnet-4.6` | Writes code — quality first | Heavy code gen → `gpt-5.3-codex` | +| Tester / QA | `claude-sonnet-4.6` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | +| Lead / Architect | auto (per-task) | Mixed: code review needs quality, planning needs cost | Architecture proposals → premium; triage/planning → haiku | +| Prompt Engineer | auto (per-task) | Mixed: prompt design is like code, research is not | Prompt architecture → sonnet; research/analysis → haiku | +| Copilot SDK Expert | `claude-sonnet-4.6` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | +| Designer / Visual | `claude-opus-4.5` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | +| DevRel / Writer | `claude-haiku-4.5` | Docs and writing — not code | — | +| Scribe / Logger | `claude-haiku-4.5` | Mechanical file ops — cheapest possible | — (never bump Scribe) | +| Git / Release | `claude-haiku-4.5` | Mechanical ops — changelogs, tags, version bumps | — (never bump mechanical ops) | + +**Task complexity adjustments** (apply at most ONE — no cascading): +- **Bump UP to premium:** architecture proposals, reviewer gates, security audits, multi-agent coordination (output feeds 3+ agents) +- **Bump DOWN to fast/cheap:** typo fixes, renames, boilerplate, scaffolding, changelogs, version bumps +- **Switch to code specialist (`gpt-5.3-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) +- **Switch to analytical diversity (`gemini-3-pro-preview`):** code reviews where a second perspective helps, security reviews, architecture reviews after a rejection + +**Layer 4 — Default:** If nothing else matched, use `claude-haiku-4.5`. Cost wins when in doubt, unless code is being produced. + +**Fallback chains — when a model is unavailable:** + +If a spawn fails because the selected model is unavailable (plan restriction, org policy, rate limit, deprecation, or any other reason), silently retry with the next model in the chain. Do NOT tell the user about fallback attempts. Maximum 3 retries before jumping to the nuclear fallback. + +``` +Premium: claude-opus-4.6 → claude-opus-4.5 → claude-sonnet-4.6 → claude-sonnet-4.5 → (omit model param) +Standard: claude-sonnet-4.6 → claude-sonnet-4.5 → gpt-5.4 → gpt-5.3-codex → claude-sonnet-4 → (omit model param) +Fast: claude-haiku-4.5 → gpt-5.4-mini → gpt-5.1-codex-mini → gpt-4.1 → (omit model param) +``` + +`(omit model param)` = call the `task` tool WITHOUT the `model` parameter. The platform uses its built-in default. This is the nuclear fallback — it always works. + +**Fallback rules:** +- If the user specified a provider ("use Claude"), fall back within that provider only before hitting nuclear +- Never fall back UP in tier — a fast/cheap task should not land on a premium model +- Log fallbacks to the orchestration log for debugging, but never surface to the user unless asked + +**Passing the model to spawns:** + +Pass the resolved model as the `model` parameter on every `task` tool call: + +``` +agent_type: "general-purpose" +model: "{resolved_model}" +mode: "background" +name: "{name}" +description: "{emoji} {Name}: {brief task summary}" +prompt: | + ... +``` + +Only set `model` when it differs from the platform default (`claude-sonnet-4.6`). If the resolved model IS `claude-sonnet-4.6`, you MAY omit the `model` parameter — the platform uses it as default. + +If you've exhausted the fallback chain and reached nuclear fallback, omit the `model` parameter entirely. + +**Spawn output format — show the model choice:** + +When spawning, include the model in your acknowledgment: + +``` +šŸ”§ Fenster (claude-sonnet-4.6) — refactoring auth module +šŸŽØ Redfoot (claude-opus-4.5 Ā· vision) — designing color system +šŸ“‹ Scribe (claude-haiku-4.5 Ā· fast) — logging session +⚔ Keaton (claude-opus-4.6 Ā· bumped for architecture) — reviewing proposal +šŸ“ McManus (claude-haiku-4.5 Ā· fast) — updating docs +``` + +Include tier annotation only when the model was bumped or a specialist was chosen. Default-tier spawns just show the model name. + +**Valid models (current platform catalog):** + +Premium: `claude-opus-4.6`, `claude-opus-4.6-1m` (Internal only), `claude-opus-4.5` +Standard: `claude-sonnet-4.6`, `claude-sonnet-4.5`, `claude-sonnet-4`, `gpt-5.4`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1`, `gemini-3-pro-preview` +Fast/Cheap: `claude-haiku-4.5`, `gpt-5.4-mini`, `gpt-5.1-codex-mini`, `gpt-5-mini`, `gpt-4.1` diff --git a/.squad/templates/multi-agent-format.md b/.squad/templates/multi-agent-format.md new file mode 100644 index 000000000..b655ee942 --- /dev/null +++ b/.squad/templates/multi-agent-format.md @@ -0,0 +1,28 @@ +# Multi-Agent Artifact Format + +When multiple agents contribute to a final artifact (document, analysis, design), use this format. The assembled result must include: + +- Termination condition +- Constraint budgets (if active) +- Reviewer verdicts (if any) +- Raw agent outputs appendix + +## Assembly Structure + +The assembled result goes at the top. Below it, include: + +``` +## APPENDIX: RAW AGENT OUTPUTS + +### {Name} ({Role}) — Raw Output +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) — Raw Output +{Paste agent's verbatim response here, unedited} +``` + +## Appendix Rules + +This appendix is for diagnostic integrity. Do not edit, summarize, or polish the raw outputs. The Coordinator may not rewrite raw agent outputs; it may only paste them verbatim and assemble the final artifact above. + +See `.squad/templates/run-output.md` for the complete output format template. diff --git a/.squad/templates/notes-protocol.md b/.squad/templates/notes-protocol.md new file mode 100644 index 000000000..5a1d03a5f --- /dev/null +++ b/.squad/templates/notes-protocol.md @@ -0,0 +1,202 @@ +# Squad Notes Protocol + +> Contract for agent state via git notes. Agents write commit-scoped context +> here instead of modifying `.squad/` files in PRs. +> +> **Version:** 1.0 +> **Backends:** `git-notes`, `orphan` + +--- + +## Overview + +Squad state has two layers: + +1. **Git notes layer** (this document) — thin, commit-scoped annotations that + attach agent context to commits without appearing in PRs or diffs. +2. **Permanent state layer** — long-lived decisions, routing rules, and archives + stored via the configured state backend (`git-notes` or `orphan` branch). + +Agents write notes during their work rounds. Ralph promotes flagged notes to +permanent state after a PR merges. + +--- + +## Namespaces + +Each agent writes to its own namespace to prevent conflicts: + +| Namespace | Owner | Purpose | +|-----------|-------|---------| +| `refs/notes/squad/data` | Data | Architecture decisions, implementation choices | +| `refs/notes/squad/worf` | Worf | Security reviews, vulnerability assessments | +| `refs/notes/squad/seven` | Seven | Documentation quality, API contract decisions | +| `refs/notes/squad/ralph` | Ralph | Work-round progress, task-state annotations | +| `refs/notes/squad/q` | Q | Devil's advocate findings, risk assessments | +| `refs/notes/squad/research` | Any agent | Research notes that should survive branch deletion | +| `refs/notes/squad/review` | Any agent | Code review context (mirrors Gerrit's pattern) | + +**Rule**: Only write to your own namespace. The shared namespaces +(`research`, `review`) use `append` — never `add`. + +--- + +## Note JSON Schema + +All notes MUST be valid JSON. Minimum required fields: + +```json +{ + "agent": "Data", + "timestamp": "2026-03-23T14:00:00Z", + "type": "decision | research | review | progress | security", + "content": "..." +} +``` + +### Decision notes + +```json +{ + "agent": "Data", + "timestamp": "2026-03-23T14:00:00Z", + "type": "decision", + "decision": "Use JWT RS256 for auth middleware", + "reasoning": "Existing pattern in codebase — auth.go:47-89.", + "alternatives_considered": ["HS256", "session tokens"], + "confidence": "high", + "promote_to_permanent": true +} +``` + +Set `"promote_to_permanent": true` to signal Ralph to copy this to +`decisions.md` after the PR merges. + +### Research notes + +```json +{ + "agent": "Data", + "timestamp": "2026-03-23T14:00:00Z", + "type": "research", + "topic": "JWT vs session tokens", + "findings": {}, + "effort_hours": 2.5, + "archive_on_close": true +} +``` + +Set `"archive_on_close": true` to signal Ralph to archive this to +`state/research/` even if the PR is rejected. + +--- + +## Write Commands + +```bash +# Write a decision note on the current commit +git notes --ref=squad/{your-agent} add \ + -m '{"agent":"{Agent}","timestamp":"...","type":"decision","decision":"..."}' \ + HEAD + +# Append to an existing note (multiple items on same commit) +git notes --ref=squad/{your-agent} append \ + -m '{"agent":"{Agent}","timestamp":"...","type":"progress","content":"..."}' \ + HEAD + +# Read your note +git notes --ref=squad/{your-agent} show HEAD + +# List all commits with notes in your namespace +git notes --ref=squad/{your-agent} list +``` + +Or use the helper script: + +```powershell +./scripts/notes/write-note.ps1 -Agent data -Type decision \ + -Content '{"decision":"Use JWT","reasoning":"..."}' \ + [-Commit HEAD] [-Promote] [-Archive] +``` + +--- + +## Fetch / Push + +**Notes are NOT fetched or pushed by default.** Every clone needs setup. + +### One-time setup + +```bash +git config --add remote.origin.fetch 'refs/notes/*:refs/notes/*' +git fetch origin 'refs/notes/*:refs/notes/*' +``` + +Or use the helper: + +```powershell +./scripts/notes/fetch.ps1 -Setup +``` + +### Every work round + +1. **Start**: `git fetch origin 'refs/notes/*:refs/notes/*'` +2. **End**: `git push origin 'refs/notes/*:refs/notes/*'` + +--- + +## Conflict Handling + +1. **Per-agent namespaces prevent 99% of conflicts.** Only one agent writes to + `refs/notes/squad/data`, so there are no write conflicts in normal use. + +2. **Same agent, two machines:** First push wins. Losing machine should fetch + and append: + ```bash + git fetch origin 'refs/notes/*:refs/notes/*' + git notes --ref=squad/{agent} append -m '{...}' HEAD + git push origin 'refs/notes/*:refs/notes/*' + ``` + +3. **Shared namespaces** (`research`, `review`): Always use `git notes append`, + never `git notes add`. + +4. **Push conflict recovery:** + ```bash + git fetch origin 'refs/notes/*:refs/notes/*' + git notes merge refs/notes/remotes/origin/squad/{namespace} + git push origin 'refs/notes/*:refs/notes/*' + ``` + +--- + +## When to Use Notes vs State Backend + +| Use git notes | Use state backend | +|---------------|-------------------| +| Why THIS choice on THIS commit | Universal routing rules, conventions | +| Decisions scoped to a feature | Long-lived decisions for all future work | +| Research for a specific investigation | Research archives (promoted from notes) | +| Security sign-offs per commit | Agent history persisting across features | +| Agent-to-agent context for current feature | Team agreements and policies | + +When in doubt: **notes first, promote to permanent state later.** Ralph handles +the promotion automatically when `promote_to_permanent` is set. + +--- + +## Ralph Promotion Rules + +**After PR merge:** + +1. Fetch all notes from remote +2. Traverse commits reachable from the default branch that have notes +3. For each note with `"promote_to_permanent": true` → append to `decisions.md` +4. Push state + +**After PR close/rejection:** + +1. List notes in `squad/research` on the closed branch's commits +2. For each note with `"archive_on_close": true` → archive to `research/` +3. Push state +4. Notes on rejected commits are NOT promoted — this is the desired behavior diff --git a/.squad/templates/orchestration-log.md b/.squad/templates/orchestration-log.md new file mode 100644 index 000000000..37d94d193 --- /dev/null +++ b/.squad/templates/orchestration-log.md @@ -0,0 +1,27 @@ +# Orchestration Log Entry + +> One file per agent spawn. Saved to `.squad/orchestration-log/{timestamp}-{agent-name}.md` + +--- + +### {timestamp} — {task summary} + +| Field | Value | +|-------|-------| +| **Agent routed** | {Name} ({Role}) | +| **Why chosen** | {Routing rationale — what in the request matched this agent} | +| **Mode** | {`background` / `sync`} | +| **Why this mode** | {Brief reason — e.g., "No hard data dependencies" or "User needs to approve architecture"} | +| **Files authorized to read** | {Exact file paths the agent was told to read} | +| **File(s) agent must produce** | {Exact file paths the agent is expected to create or modify} | +| **Outcome** | {Completed / Rejected by {Reviewer} / Escalated} | + +--- + +## Rules + +1. **One file per agent spawn.** Named `{timestamp}-{agent-name}.md`. +2. **Log BEFORE spawning.** The entry must exist before the agent runs. +3. **Update outcome AFTER the agent completes.** Fill in the Outcome field. +4. **Never delete or edit past entries.** Append-only. +5. **If a reviewer rejects work,** log the rejection as a new entry with the revision agent. diff --git a/.squad/templates/package.json b/.squad/templates/package.json new file mode 100644 index 000000000..140154e8d --- /dev/null +++ b/.squad/templates/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/.squad/templates/plugin-marketplace.md b/.squad/templates/plugin-marketplace.md new file mode 100644 index 000000000..893632816 --- /dev/null +++ b/.squad/templates/plugin-marketplace.md @@ -0,0 +1,49 @@ +# Plugin Marketplace + +Plugins are curated agent templates, skills, instructions, and prompts shared by the community via GitHub repositories (e.g., `github/awesome-copilot`, `anthropics/skills`). They provide ready-made expertise for common domains — cloud platforms, frameworks, testing strategies, etc. + +## Marketplace State + +Registered marketplace sources are stored in `.squad/plugins/marketplaces.json`: + +```json +{ + "marketplaces": [ + { + "name": "awesome-copilot", + "source": "github/awesome-copilot", + "added_at": "2026-02-14T00:00:00Z" + } + ] +} +``` + +## CLI Commands + +Users manage marketplaces via the CLI: +- `squad plugin marketplace add {owner/repo}` — Register a GitHub repo as a marketplace source +- `squad plugin marketplace remove {name}` — Remove a registered marketplace +- `squad plugin marketplace list` — List registered marketplaces +- `squad plugin marketplace browse {name}` — List available plugins in a marketplace + +## When to Browse + +During the **Adding Team Members** flow, AFTER allocating a name but BEFORE generating the charter: + +1. Read `.squad/plugins/marketplaces.json`. If the file doesn't exist or `marketplaces` is empty, skip silently. +2. For each registered marketplace, search for plugins whose name or description matches the new member's role or domain keywords. +3. Present matching plugins to the user: *"Found '{plugin-name}' in {marketplace} marketplace — want me to install it as a skill for {CastName}?"* +4. If the user accepts, install the plugin (see below). If they decline or skip, proceed without it. + +## How to Install a Plugin + +1. Read the plugin content from the marketplace repository (the plugin's `SKILL.md` or equivalent). +2. Copy it into the agent's skills directory: `.squad/skills/{plugin-name}/SKILL.md` +3. If the plugin includes charter-level instructions (role boundaries, tool preferences), merge those into the agent's `charter.md`. +4. Log the installation in the agent's `history.md`: *"šŸ“¦ Plugin '{plugin-name}' installed from {marketplace}."* + +## Graceful Degradation + +- **No marketplaces configured:** Skip the marketplace check entirely. No warning, no prompt. +- **Marketplace unreachable:** Warn the user (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and proceed with team member creation normally. +- **No matching plugins:** Inform the user (*"No matching plugins found in configured marketplaces"*) and proceed. diff --git a/.squad/templates/prd-intake.md b/.squad/templates/prd-intake.md new file mode 100644 index 000000000..4bc2438c6 --- /dev/null +++ b/.squad/templates/prd-intake.md @@ -0,0 +1,105 @@ +# PRD Intake + +On-demand reference for ingesting a PRD, decomposing it into work items, and managing updates. + +## Triggers + +| User says | Action | +|-----------|--------| +| "here's the PRD" / "work from this spec" | Expect file path or pasted content | +| "read the PRD at {path}" | Read the file at that path | +| "the PRD changed" / "updated the spec" | Re-read and diff against previous decomposition | +| (pastes requirements text) | Treat as inline PRD | + +## Intake Flow + +1. **Detect source:** File path, pasted text, or URL. Store a reference in `.squad/team.md` under `## PRD Source`. +2. **Store PRD reference:** + ```markdown + ## PRD Source + + **Path:** {path-or-inline} + **Ingested:** {ISO date} + **Hash:** {sha256 of content, for change detection} + ``` +3. **Spawn Lead (sync, premium bump)** with decomposition prompt (see below). +4. **Present work items** to user for approval in table format. +5. **On approval:** Route items to agents respecting dependency order. + +## Lead Decomposition Spawn Template + +``` +You are the Lead, decomposing a PRD into actionable work items. + +PRD CONTENT: +{full PRD text} + +TEAM ROSTER: +{roster from team.md} + +TASK: Break this PRD into discrete, implementable work items. For each item provide: +- Title (imperative mood, concise) +- Description (acceptance criteria, technical notes) +- Estimated complexity: S / M / L +- Dependencies (list other item titles this blocks on) +- Suggested assignee (agent name from roster, based on expertise match) + +OUTPUT FORMAT: +Return a markdown table: + +| # | Title | Complexity | Dependencies | Assignee | Status | +|---|-------|-----------|--------------|----------|--------| +| 1 | {title} | {S/M/L} | — | {agent} | pending | + +RULES: +- Items must be independently implementable (no item requires partial completion of another). +- Maximum 1 day of work per item (split larger items). +- Respect team expertise — don't assign frontend work to a backend specialist. +- Order by dependency graph (items with no deps first). +- Flag any ambiguities or missing information as "āš ļø Needs clarification: {question}". +``` + +## Work Item Presentation Format + +Present to user as: + +``` +šŸ“‹ PRD decomposed into {N} work items: + +| # | Title | Size | Depends on | Assignee | +|---|-------|------|-----------|----------| +| 1 | ... | S | — | {Agent} | +| 2 | ... | M | #1 | {Agent} | + +Ready to proceed? I'll route items respecting the dependency order. +āš ļø Clarifications needed: {list any flagged items} +``` + +## Mid-Project Updates + +When the user says the PRD changed: + +1. Re-read the PRD content. +2. Compute diff against stored hash. +3. Spawn Lead (sync) with a delta-decomposition prompt: + - Show only NEW or CHANGED sections. + - Ask Lead to identify: new items, modified items, obsoleted items. +4. Present changes to user: + ``` + šŸ“‹ PRD update detected: + - New items: {count} + - Modified: {count} + - Obsoleted: {count} (will be cancelled if approved) + + {table of changes} + + Approve these updates? + ``` +5. On approval: Cancel obsoleted work (if not yet started), update items, re-route. + +## State Tracking + +Active PRD state lives in team.md: +- `## PRD Source` section (path, date, hash) +- Work items tracked as issues (GitHub) or in `.squad/backlog.md` (offline mode) +- Completion percentage displayed in status checks diff --git a/.squad/templates/ralph-circuit-breaker.md b/.squad/templates/ralph-circuit-breaker.md new file mode 100644 index 000000000..87be26015 --- /dev/null +++ b/.squad/templates/ralph-circuit-breaker.md @@ -0,0 +1,313 @@ +# Ralph Circuit Breaker — Model Rate Limit Fallback + +> Classic circuit breaker pattern (Hystrix / Polly / Resilience4j) applied to Copilot model selection. +> When the preferred model hits rate limits, Ralph automatically degrades to free-tier models, then self-heals. + +## Problem + +When running multiple Ralph instances across repos, Copilot model rate limits cause cascading failures. +All Ralphs fail simultaneously when the preferred model (e.g., `claude-sonnet-4.6`) hits quota. + +Premium models burn quota fast: +| Model | Multiplier | Risk | +|-------|-----------|------| +| `claude-sonnet-4.6` | 1x | Moderate with many Ralphs | +| `claude-opus-4.6` | 10x | High | +| `gpt-5.4` | 50x | Very high | +| `gpt-5.4-mini` | **0x** | **Free — unlimited** | +| `gpt-5-mini` | **0x** | **Free — unlimited** | +| `gpt-4.1` | **0x** | **Free — unlimited** | + +## Circuit Breaker States + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” rate limit error ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ CLOSED │ ───────────────────► │ OPEN │ +│ (normal)│ │(fallback)│ +ā””ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”˜ ◄──────────────── ā””ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”˜ + │ 2 consecutive │ + │ successes │ cooldown expires + │ ā–¼ + │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + └───── success ◄──────── │HALF-OPEN │ + (close) │ (testing) │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +### CLOSED (normal operation) +- Use preferred model from config +- Every successful response confirms circuit stays closed +- On rate limit error → transition to OPEN + +### OPEN (rate limited — fallback active) +- Fall back through the free-tier model chain: + 1. `gpt-5.4-mini` + 2. `gpt-5-mini` + 3. `gpt-4.1` +- Start cooldown timer (default: 10 minutes) +- When cooldown expires → transition to HALF-OPEN + +### HALF-OPEN (testing recovery) +- Try preferred model again +- If 2 consecutive successes → transition to CLOSED +- If rate limit error → back to OPEN, reset cooldown + +## State File: `.squad/ralph-circuit-breaker.json` + +```json +{ + "state": "closed", + "preferredModel": "claude-sonnet-4.6", + "fallbackChain": ["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"], + "currentFallbackIndex": 0, + "cooldownMinutes": 10, + "openedAt": null, + "halfOpenSuccesses": 0, + "consecutiveFailures": 0, + "metrics": { + "totalFallbacks": 0, + "totalRecoveries": 0, + "lastFallbackAt": null, + "lastRecoveryAt": null + } +} +``` + +## PowerShell Functions + +Paste these into your `ralph-watch.ps1` or source them from a shared module. + +### `Get-CircuitBreakerState` + +```powershell +function Get-CircuitBreakerState { + param([string]$StateFile = ".squad/ralph-circuit-breaker.json") + + if (-not (Test-Path $StateFile)) { + $default = @{ + state = "closed" + preferredModel = "claude-sonnet-4.6" + fallbackChain = @("gpt-5.4-mini", "gpt-5-mini", "gpt-4.1") + currentFallbackIndex = 0 + cooldownMinutes = 10 + openedAt = $null + halfOpenSuccesses = 0 + consecutiveFailures = 0 + metrics = @{ + totalFallbacks = 0 + totalRecoveries = 0 + lastFallbackAt = $null + lastRecoveryAt = $null + } + } + $default | ConvertTo-Json -Depth 3 | Set-Content $StateFile + return $default + } + + return (Get-Content $StateFile -Raw | ConvertFrom-Json) +} +``` + +### `Save-CircuitBreakerState` + +```powershell +function Save-CircuitBreakerState { + param( + [object]$State, + [string]$StateFile = ".squad/ralph-circuit-breaker.json" + ) + + $State | ConvertTo-Json -Depth 3 | Set-Content $StateFile +} +``` + +### `Get-CurrentModel` + +Returns the model Ralph should use right now, based on circuit state. + +```powershell +function Get-CurrentModel { + param([string]$StateFile = ".squad/ralph-circuit-breaker.json") + + $cb = Get-CircuitBreakerState -StateFile $StateFile + + switch ($cb.state) { + "closed" { + return $cb.preferredModel + } + "open" { + # Check if cooldown has expired + if ($cb.openedAt) { + $opened = [DateTime]::Parse($cb.openedAt) + $elapsed = (Get-Date) - $opened + if ($elapsed.TotalMinutes -ge $cb.cooldownMinutes) { + # Transition to half-open + $cb.state = "half-open" + $cb.halfOpenSuccesses = 0 + Save-CircuitBreakerState -State $cb -StateFile $StateFile + Write-Host " [circuit-breaker] Cooldown expired. Testing preferred model..." -ForegroundColor Yellow + return $cb.preferredModel + } + } + # Still in cooldown — use fallback + $idx = [Math]::Min($cb.currentFallbackIndex, $cb.fallbackChain.Count - 1) + return $cb.fallbackChain[$idx] + } + "half-open" { + return $cb.preferredModel + } + default { + return $cb.preferredModel + } + } +} +``` + +### `Update-CircuitBreakerOnSuccess` + +Call after every successful model response. + +```powershell +function Update-CircuitBreakerOnSuccess { + param([string]$StateFile = ".squad/ralph-circuit-breaker.json") + + $cb = Get-CircuitBreakerState -StateFile $StateFile + $cb.consecutiveFailures = 0 + + if ($cb.state -eq "half-open") { + $cb.halfOpenSuccesses++ + if ($cb.halfOpenSuccesses -ge 2) { + # Recovery! Close the circuit + $cb.state = "closed" + $cb.openedAt = $null + $cb.halfOpenSuccesses = 0 + $cb.currentFallbackIndex = 0 + $cb.metrics.totalRecoveries++ + $cb.metrics.lastRecoveryAt = (Get-Date).ToString("o") + Save-CircuitBreakerState -State $cb -StateFile $StateFile + Write-Host " [circuit-breaker] RECOVERED — back to preferred model ($($cb.preferredModel))" -ForegroundColor Green + return + } + Save-CircuitBreakerState -State $cb -StateFile $StateFile + Write-Host " [circuit-breaker] Half-open success $($cb.halfOpenSuccesses)/2" -ForegroundColor Yellow + return + } + + # closed state — nothing to do +} +``` + +### `Update-CircuitBreakerOnRateLimit` + +Call when a model response indicates rate limiting (HTTP 429 or error message containing "rate limit"). + +```powershell +function Update-CircuitBreakerOnRateLimit { + param([string]$StateFile = ".squad/ralph-circuit-breaker.json") + + $cb = Get-CircuitBreakerState -StateFile $StateFile + $cb.consecutiveFailures++ + + if ($cb.state -eq "closed" -or $cb.state -eq "half-open") { + # Open the circuit + $cb.state = "open" + $cb.openedAt = (Get-Date).ToString("o") + $cb.halfOpenSuccesses = 0 + $cb.currentFallbackIndex = 0 + $cb.metrics.totalFallbacks++ + $cb.metrics.lastFallbackAt = (Get-Date).ToString("o") + Save-CircuitBreakerState -State $cb -StateFile $StateFile + + $fallbackModel = $cb.fallbackChain[0] + Write-Host " [circuit-breaker] RATE LIMITED — falling back to $fallbackModel (cooldown: $($cb.cooldownMinutes)m)" -ForegroundColor Red + return + } + + if ($cb.state -eq "open") { + # Already open — try next fallback in chain if current one also fails + if ($cb.currentFallbackIndex -lt ($cb.fallbackChain.Count - 1)) { + $cb.currentFallbackIndex++ + $nextModel = $cb.fallbackChain[$cb.currentFallbackIndex] + Write-Host " [circuit-breaker] Fallback also limited — trying $nextModel" -ForegroundColor Red + } + # Reset cooldown timer + $cb.openedAt = (Get-Date).ToString("o") + Save-CircuitBreakerState -State $cb -StateFile $StateFile + } +} +``` + +## Integration with ralph-watch.ps1 + +In your Ralph polling loop, wrap the model selection: + +```powershell +# At the top of your polling loop +$model = Get-CurrentModel + +# When invoking copilot CLI +$result = copilot-cli --model $model ... + +# After the call +if ($result -match "rate.?limit" -or $LASTEXITCODE -eq 429) { + Update-CircuitBreakerOnRateLimit +} else { + Update-CircuitBreakerOnSuccess +} +``` + +### Full integration example + +```powershell +# Source the circuit breaker functions +. .squad-templates/ralph-circuit-breaker-functions.ps1 + +while ($true) { + $model = Get-CurrentModel + Write-Host "Polling with model: $model" + + try { + # Your existing Ralph logic here, but pass $model + $response = Invoke-RalphCycle -Model $model + + # Success path + Update-CircuitBreakerOnSuccess + } + catch { + if ($_.Exception.Message -match "rate.?limit|429|quota|Too Many Requests") { + Update-CircuitBreakerOnRateLimit + # Retry immediately with fallback model + continue + } + # Other errors — handle normally + throw + } + + Start-Sleep -Seconds $pollInterval +} +``` + +## Configuration + +Override defaults by editing `.squad/ralph-circuit-breaker.json`: + +| Field | Default | Description | +|-------|---------|-------------| +| `preferredModel` | `claude-sonnet-4.6` | Model to use when circuit is closed | +| `fallbackChain` | `["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"]` | Ordered fallback models (all free-tier) | +| `cooldownMinutes` | `10` | How long to wait before testing recovery | + +## Metrics + +The state file tracks operational metrics: + +- **totalFallbacks** — How many times the circuit opened +- **totalRecoveries** — How many times it recovered to preferred model +- **lastFallbackAt** — ISO timestamp of last rate limit event +- **lastRecoveryAt** — ISO timestamp of last successful recovery + +Query metrics with: +```powershell +$cb = Get-Content .squad/ralph-circuit-breaker.json | ConvertFrom-Json +Write-Host "Fallbacks: $($cb.metrics.totalFallbacks) | Recoveries: $($cb.metrics.totalRecoveries)" +``` diff --git a/.squad/templates/ralph-reference.md b/.squad/templates/ralph-reference.md new file mode 100644 index 000000000..3d8b2b440 --- /dev/null +++ b/.squad/templates/ralph-reference.md @@ -0,0 +1,141 @@ +# Ralph Reference + +## Ralph — Work Monitor + +Ralph is a built-in squad member whose job is keeping tabs on work. **Ralph tracks and drives the work queue.** Always on the roster, one job: make sure the team never sits idle. + +**⚔ CRITICAL BEHAVIOR: When Ralph is active, the coordinator MUST NOT stop and wait for user input between work items. Ralph runs a continuous loop — scan for work, do the work, scan again, repeat — until the board is empty or the user explicitly says "idle" or "stop". This is not optional. If work exists, keep going. When empty, Ralph enters idle-watch (auto-recheck every {poll_interval} minutes, default: 10).** + +**Between checks:** Ralph's in-session loop runs while work exists. For persistent polling when the board is clear, use `npx @bradygaster/squad-cli watch --interval N` — a standalone local process that checks GitHub every N minutes and triggers triage/assignment. See [Watch Mode](#watch-mode-squad-watch). + +**On-demand reference:** Read `.squad/templates/ralph-reference.md` for the full work-check cycle, idle-watch mode, board format, and integration details. + +### Roster Entry + +Ralph always appears in `team.md`: `| Ralph | Work Monitor | — | šŸ”„ Monitor |` + +### Triggers + +| User says | Action | +|-----------|--------| +| "Ralph, go" / "Ralph, start monitoring" / "keep working" | Activate work-check loop | +| "Ralph, status" / "What's on the board?" / "How's the backlog?" | Run one work-check cycle, report results, don't loop | +| "Ralph, check every N minutes" | Set idle-watch polling interval | +| "Ralph, idle" / "Take a break" / "Stop monitoring" | Fully deactivate (stop loop + idle-watch) | +| "Ralph, scope: just issues" / "Ralph, skip CI" | Adjust what Ralph monitors this session | +| References PR feedback or changes requested | Spawn agent to address PR review feedback | +| "merge PR #N" / "merge it" (recent context) | Merge via `gh pr merge` | + +These are intent signals, not exact strings — match meaning, not words. + +When Ralph is active, run this check cycle after every batch of agent work completes (or immediately on activation): + +**Step 1 — Scan for work** (run these in parallel): + +```bash +# Untriaged issues (labeled squad but no squad:{member} sub-label) +gh issue list --label "squad" --state open --json number,title,labels,assignees --limit 20 + +# Member-assigned issues (labeled squad:{member}, still open) +gh issue list --state open --json number,title,labels,assignees --limit 20 | # filter for squad:* labels + +# Open PRs from squad members +gh pr list --state open --json number,title,author,labels,isDraft,reviewDecision --limit 20 + +# Draft PRs (agent work in progress) +gh pr list --state open --draft --json number,title,author,labels,checks --limit 20 +``` + +**Step 2 — Categorize findings:** + +| Category | Signal | Action | +|----------|--------|--------| +| **Untriaged issues** | `squad` label, no `squad:{member}` label | Lead triages: reads issue, assigns `squad:{member}` label | +| **Assigned but unstarted** | `squad:{member}` label, no assignee or no PR | Spawn the assigned agent to pick it up | +| **Draft PRs** | PR in draft from squad member | Check if agent needs to continue; if stalled, nudge | +| **Review feedback** | PR has `CHANGES_REQUESTED` review | Route feedback to PR author agent to address | +| **CI failures** | PR checks failing | Notify assigned agent to fix, or create a fix issue | +| **Approved PRs** | PR approved, CI green, ready to merge | Merge and close related issue | +| **No work found** | All clear | Report: "šŸ“‹ Board is clear. Ralph is idling." Suggest `npx @bradygaster/squad-cli watch` for persistent polling. | + +**Step 3 — Act on highest-priority item:** +- Process one category at a time, highest priority first (untriaged > assigned > CI failures > review feedback > approved PRs) +- Spawn agents as needed, collect results +- **⚔ CRITICAL: After results are collected, DO NOT stop. DO NOT wait for user input. IMMEDIATELY go back to Step 1 and scan again.** This is a loop — Ralph keeps cycling until the board is clear or the user says "idle". Each cycle is one "round". +- If multiple items exist in the same category, process them in parallel (spawn multiple agents) + +**Step 4 — Periodic check-in** (every 3-5 rounds): + +After every 3-5 rounds, pause and report before continuing: + +``` +šŸ”„ Ralph: Round {N} complete. + āœ… {X} issues closed, {Y} PRs merged + šŸ“‹ {Z} items remaining: {brief list} + Continuing... (say "Ralph, idle" to stop) +``` + +**Do NOT ask for permission to continue.** Just report and keep going. The user must explicitly say "idle" or "stop" to break the loop. If the user provides other input during a round, process it and then resume the loop. + +### Watch Mode (`squad watch`) + +Ralph's in-session loop processes work while it exists, then idles. For **persistent polling** between sessions or when you're away from the keyboard, use the `squad watch` CLI command: + +```bash +npx @bradygaster/squad-cli watch # polls every 10 minutes (default) +npx @bradygaster/squad-cli watch --interval 5 # polls every 5 minutes +npx @bradygaster/squad-cli watch --interval 30 # polls every 30 minutes +``` + +This runs as a standalone local process (not inside Copilot) that: +- Checks GitHub every N minutes for untriaged squad work +- Auto-triages issues based on team roles and keywords +- Assigns @copilot to `squad:copilot` issues (if auto-assign is enabled) +- Runs until Ctrl+C + +**Three layers of Ralph:** + +| Layer | When | How | +|-------|------|-----| +| **In-session** | You're at the keyboard | "Ralph, go" — active loop while work exists | +| **Local watchdog** | You're away but machine is on | `npx @bradygaster/squad-cli watch --interval 10` | +| **Cloud heartbeat** | Fully unattended | `squad-heartbeat.yml` — event-based only (cron disabled) | + +### Ralph State + +Ralph's state is session-scoped (not persisted to disk): +- **Active/idle** — whether the loop is running +- **Round count** — how many check cycles completed +- **Scope** — what categories to monitor (default: all) +- **Stats** — issues closed, PRs merged, items processed this session + +### Ralph on the Board + +When Ralph reports status, use this format: + +``` +šŸ”„ Ralph — Work Monitor +━━━━━━━━━━━━━━━━━━━━━━ +šŸ“Š Board Status: + šŸ”“ Untriaged: 2 issues need triage + 🟔 In Progress: 3 issues assigned, 1 draft PR + 🟢 Ready: 1 PR approved, awaiting merge + āœ… Done: 5 issues closed this session + +Next action: Triaging #42 — "Fix auth endpoint timeout" +``` + +### Integration with Follow-Up Work + +After the coordinator's step 6 ("Immediately assess: Does anything trigger follow-up work?"), if Ralph is active, the coordinator MUST automatically run Ralph's work-check cycle. **Do NOT return control to the user.** This creates a continuous pipeline: + +1. User activates Ralph → work-check cycle runs +2. Work found → agents spawned → results collected +3. Follow-up work assessed → more agents if needed +4. Ralph scans GitHub again (Step 1) → IMMEDIATELY, no pause +5. More work found → repeat from step 2 +6. No more work → "šŸ“‹ Board is clear. Ralph is idling." (suggest `npx @bradygaster/squad-cli watch` for persistent polling) + +**Ralph does NOT ask "should I continue?" — Ralph KEEPS GOING.** Only stops on explicit "idle"/"stop" or session end. A clear board → idle-watch, not full stop. For persistent monitoring after the board clears, use `npx @bradygaster/squad-cli watch`. + +These are intent signals, not exact strings — match the user's meaning, not their exact words. diff --git a/.squad/templates/ralph-triage.js b/.squad/templates/ralph-triage.js new file mode 100644 index 000000000..0f533ee48 --- /dev/null +++ b/.squad/templates/ralph-triage.js @@ -0,0 +1,545 @@ +#!/usr/bin/env node +/** + * Ralph Triage Script — Standalone CJS implementation + * + * āš ļø SYNC NOTICE: This file ports triage logic from the SDK source: + * packages/squad-sdk/src/ralph/triage.ts + * + * Any changes to routing/triage logic MUST be applied to BOTH files. + * The SDK module is the canonical implementation; this script exists + * for zero-dependency use in GitHub Actions workflows. + * + * To verify parity: npm test -- test/ralph-triage.test.ts + */ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const https = require('node:https'); +const { execSync } = require('node:child_process'); + +function parseArgs(argv) { + let squadDir = '.squad'; + let output = 'triage-results.json'; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--squad-dir') { + squadDir = argv[i + 1]; + i += 1; + continue; + } + if (arg === '--output') { + output = argv[i + 1]; + i += 1; + continue; + } + if (arg === '--help' || arg === '-h') { + printUsage(); + process.exit(0); + } + throw new Error(`Unknown argument: ${arg}`); + } + + if (!squadDir) throw new Error('--squad-dir requires a value'); + if (!output) throw new Error('--output requires a value'); + + return { squadDir, output }; +} + +function printUsage() { + console.log('Usage: node .squad/templates/ralph-triage.js --squad-dir .squad --output triage-results.json'); +} + +function normalizeEol(content) { + return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); +} + +function slugify(text) { return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } + +function parseRoutingRules(routingMd) { + const table = parseTableSection(routingMd, /^##\s*work\s*type\s*(?:→|->)\s*agent\b/i); + if (!table) return []; + + const workTypeIndex = findColumnIndex(table.headers, ['work type', 'type']); + const agentIndex = findColumnIndex(table.headers, ['agent', 'route to', 'route']); + const examplesIndex = findColumnIndex(table.headers, ['examples', 'example']); + + if (workTypeIndex < 0 || agentIndex < 0) return []; + + const rules = []; + for (const row of table.rows) { + const workType = cleanCell(row[workTypeIndex] || ''); + const agentName = cleanCell(row[agentIndex] || ''); + const keywords = splitKeywords(examplesIndex >= 0 ? row[examplesIndex] : ''); + if (!workType || !agentName) continue; + rules.push({ workType, agentName, keywords }); + } + + return rules; +} + +function parseModuleOwnership(routingMd) { + const table = parseTableSection(routingMd, /^##\s*module\s*ownership\b/i); + if (!table) return []; + + const moduleIndex = findColumnIndex(table.headers, ['module', 'path']); + const primaryIndex = findColumnIndex(table.headers, ['primary']); + const secondaryIndex = findColumnIndex(table.headers, ['secondary']); + + if (moduleIndex < 0 || primaryIndex < 0) return []; + + const modules = []; + for (const row of table.rows) { + const modulePath = normalizeModulePath(row[moduleIndex] || ''); + const primary = cleanCell(row[primaryIndex] || ''); + const secondaryRaw = cleanCell(secondaryIndex >= 0 ? row[secondaryIndex] || '' : ''); + const secondary = normalizeOptionalOwner(secondaryRaw); + + if (!modulePath || !primary) continue; + modules.push({ modulePath, primary, secondary }); + } + + return modules; +} + +function parseRoster(teamMd) { + const table = + parseTableSection(teamMd, /^##\s*members\b/i) || + parseTableSection(teamMd, /^##\s*team\s*roster\b/i); + + if (!table) return []; + + const nameIndex = findColumnIndex(table.headers, ['name']); + const roleIndex = findColumnIndex(table.headers, ['role']); + if (nameIndex < 0 || roleIndex < 0) return []; + + const excluded = new Set(['scribe', 'ralph']); + const members = []; + + for (const row of table.rows) { + const name = cleanCell(row[nameIndex] || ''); + const role = cleanCell(row[roleIndex] || ''); + if (!name || !role) continue; + if (excluded.has(name.toLowerCase())) continue; + + members.push({ + name, + role, + label: `squad:${slugify(name)}`, + }); + } + + return members; +} + +function triageIssue(issue, rules, modules, roster) { + const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase(); + const normalizedIssueText = normalizeTextForPathMatch(issueText); + + const bestModule = findBestModuleMatch(normalizedIssueText, modules); + if (bestModule) { + const primaryMember = findMember(bestModule.primary, roster); + if (primaryMember) { + return { + agent: primaryMember, + reason: `Matched module path "${bestModule.modulePath}" to primary owner "${bestModule.primary}"`, + source: 'module-ownership', + confidence: 'high', + }; + } + + if (bestModule.secondary) { + const secondaryMember = findMember(bestModule.secondary, roster); + if (secondaryMember) { + return { + agent: secondaryMember, + reason: `Matched module path "${bestModule.modulePath}" to secondary owner "${bestModule.secondary}"`, + source: 'module-ownership', + confidence: 'medium', + }; + } + } + } + + const bestRule = findBestRuleMatch(issueText, rules); + if (bestRule) { + const agent = findMember(bestRule.rule.agentName, roster); + if (agent) { + return { + agent, + reason: `Matched routing keyword(s): ${bestRule.matchedKeywords.join(', ')}`, + source: 'routing-rule', + confidence: bestRule.matchedKeywords.length >= 2 ? 'high' : 'medium', + }; + } + } + + const roleMatch = findRoleKeywordMatch(issueText, roster); + if (roleMatch) { + return { + agent: roleMatch.agent, + reason: roleMatch.reason, + source: 'role-keyword', + confidence: 'medium', + }; + } + + const lead = findLeadFallback(roster); + if (!lead) return null; + + return { + agent: lead, + reason: 'No module, routing, or role keyword match — routed to Lead/Architect', + source: 'lead-fallback', + confidence: 'low', + }; +} + +function parseTableSection(markdown, sectionHeader) { + const lines = normalizeEol(markdown).split('\n'); + let inSection = false; + const tableLines = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!inSection && sectionHeader.test(trimmed)) { + inSection = true; + continue; + } + if (inSection && /^##\s+/.test(trimmed)) break; + if (inSection && trimmed.startsWith('|')) tableLines.push(trimmed); + } + + if (tableLines.length === 0) return null; + + let headers = null; + const rows = []; + + for (const line of tableLines) { + const cells = parseTableLine(line); + if (cells.length === 0) continue; + if (cells.every((cell) => /^:?-{2,}:?$/.test(cell))) continue; + + if (!headers) { + headers = cells; + continue; + } + + rows.push(cells); + } + + if (!headers) return null; + return { headers, rows }; +} + +function parseTableLine(line) { + return line + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((cell) => cell.trim()); +} + +function findColumnIndex(headers, candidates) { + const normalizedHeaders = headers.map((header) => cleanCell(header).toLowerCase()); + for (const candidate of candidates) { + const index = normalizedHeaders.findIndex((header) => header.includes(candidate)); + if (index >= 0) return index; + } + return -1; +} + +function cleanCell(value) { + return value + .replace(/`/g, '') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .trim(); +} + +function splitKeywords(examplesCell) { + if (!examplesCell) return []; + return examplesCell + .split(',') + .map((keyword) => cleanCell(keyword)) + .filter((keyword) => keyword.length > 0); +} + +function normalizeOptionalOwner(owner) { + if (!owner) return null; + if (/^[-—–]+$/.test(owner)) return null; + return owner; +} + +function normalizeModulePath(modulePath) { + return cleanCell(modulePath).replace(/\\/g, '/').toLowerCase(); +} + +function normalizeTextForPathMatch(text) { + return text.replace(/\\/g, '/').replace(/`/g, ''); +} + +function normalizeName(value) { + return cleanCell(value) + .toLowerCase() + .replace(/[^\w@\s-]/g, '') + .replace(/\s+/g, ' ') + .trim(); +} + +function findMember(target, roster) { + const normalizedTarget = normalizeName(target); + if (!normalizedTarget) return null; + + for (const member of roster) { + if (normalizeName(member.name) === normalizedTarget) return member; + } + + for (const member of roster) { + if (normalizeName(member.role) === normalizedTarget) return member; + } + + for (const member of roster) { + const memberName = normalizeName(member.name); + if (normalizedTarget.includes(memberName) || memberName.includes(normalizedTarget)) { + return member; + } + } + + for (const member of roster) { + const memberRole = normalizeName(member.role); + if (normalizedTarget.includes(memberRole) || memberRole.includes(normalizedTarget)) { + return member; + } + } + + return null; +} + +function findBestModuleMatch(issueText, modules) { + let best = null; + let bestLength = -1; + + for (const module of modules) { + const modulePath = normalizeModulePath(module.modulePath); + if (!modulePath) continue; + if (!issueText.includes(modulePath)) continue; + + if (modulePath.length > bestLength) { + best = module; + bestLength = modulePath.length; + } + } + + return best; +} + +function findBestRuleMatch(issueText, rules) { + let best = null; + let bestScore = 0; + + for (const rule of rules) { + const matchedKeywords = rule.keywords + .map((keyword) => keyword.toLowerCase()) + .filter((keyword) => keyword.length > 0 && issueText.includes(keyword)); + + if (matchedKeywords.length === 0) continue; + + const score = + matchedKeywords.length * 100 + matchedKeywords.reduce((sum, keyword) => sum + keyword.length, 0); + if (score > bestScore) { + best = { rule, matchedKeywords }; + bestScore = score; + } + } + + return best; +} + +function findRoleKeywordMatch(issueText, roster) { + for (const member of roster) { + const role = member.role.toLowerCase(); + + if ( + (role.includes('frontend') || role.includes('ui')) && + (issueText.includes('ui') || issueText.includes('frontend') || issueText.includes('css')) + ) { + return { agent: member, reason: 'Matched frontend/UI role keywords' }; + } + + if ( + (role.includes('backend') || role.includes('api') || role.includes('server')) && + (issueText.includes('api') || issueText.includes('backend') || issueText.includes('database')) + ) { + return { agent: member, reason: 'Matched backend/API role keywords' }; + } + + if ( + (role.includes('test') || role.includes('qa')) && + (issueText.includes('test') || issueText.includes('bug') || issueText.includes('fix')) + ) { + return { agent: member, reason: 'Matched testing/QA role keywords' }; + } + } + + return null; +} + +function findLeadFallback(roster) { + return ( + roster.find((member) => { + const role = member.role.toLowerCase(); + return role.includes('lead') || role.includes('architect'); + }) || null + ); +} + +function parseOwnerRepoFromRemote(remoteUrl) { + const sshMatch = remoteUrl.match(/^git@[^:]+:([^/]+)\/(.+?)(?:\.git)?$/); + if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] }; + + if (remoteUrl.startsWith('http://') || remoteUrl.startsWith('https://') || remoteUrl.startsWith('ssh://')) { + const parsed = new URL(remoteUrl); + const parts = parsed.pathname.replace(/^\/+/, '').replace(/\.git$/, '').split('/'); + if (parts.length >= 2) { + return { owner: parts[0], repo: parts[1] }; + } + } + + throw new Error(`Unable to parse owner/repo from remote URL: ${remoteUrl}`); +} + +function getOwnerRepoFromGit() { + const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim(); + return parseOwnerRepoFromRemote(remoteUrl); +} + +function githubRequestJson(pathname, token) { + return new Promise((resolve, reject) => { + const req = https.request( + { + hostname: 'api.github.com', + method: 'GET', + path: pathname, + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'squad-ralph-triage', + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + (res) => { + let body = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + body += chunk; + }); + res.on('end', () => { + if ((res.statusCode || 500) >= 400) { + reject(new Error(`GitHub API ${res.statusCode}: ${body}`)); + return; + } + try { + resolve(JSON.parse(body)); + } catch (error) { + reject(new Error(`Failed to parse GitHub response: ${error.message}`)); + } + }); + }, + ); + req.on('error', reject); + req.end(); + }); +} + +async function fetchSquadIssues(owner, repo, token) { + const all = []; + let page = 1; + const perPage = 100; + + for (;;) { + const query = new URLSearchParams({ + state: 'open', + labels: 'squad', + per_page: String(perPage), + page: String(page), + }); + const issues = await githubRequestJson(`/repos/${owner}/${repo}/issues?${query.toString()}`, token); + if (!Array.isArray(issues) || issues.length === 0) break; + all.push(...issues); + if (issues.length < perPage) break; + page += 1; + } + + return all; +} + +function issueHasLabel(issue, labelName) { + const target = labelName.toLowerCase(); + return (issue.labels || []).some((label) => { + if (!label) return false; + const name = typeof label === 'string' ? label : label.name; + return typeof name === 'string' && name.toLowerCase() === target; + }); +} + +function isUntriagedIssue(issue, memberLabels) { + if (issue.pull_request) return false; + if (!issueHasLabel(issue, 'squad')) return false; + return !memberLabels.some((label) => issueHasLabel(issue, label)); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const token = process.env.GITHUB_TOKEN; + if (!token) { + throw new Error('GITHUB_TOKEN is required'); + } + + const squadDir = path.resolve(process.cwd(), args.squadDir); + const teamMd = fs.readFileSync(path.join(squadDir, 'team.md'), 'utf8'); + const routingMd = fs.readFileSync(path.join(squadDir, 'routing.md'), 'utf8'); + + const roster = parseRoster(teamMd); + const rules = parseRoutingRules(routingMd); + const modules = parseModuleOwnership(routingMd); + + const { owner, repo } = getOwnerRepoFromGit(); + const openSquadIssues = await fetchSquadIssues(owner, repo, token); + + const memberLabels = roster.map((member) => member.label); + const untriaged = openSquadIssues.filter((issue) => isUntriagedIssue(issue, memberLabels)); + + const results = []; + for (const issue of untriaged) { + const decision = triageIssue( + { + number: issue.number, + title: issue.title || '', + body: issue.body || '', + labels: [], + }, + rules, + modules, + roster, + ); + + if (!decision) continue; + results.push({ + issueNumber: issue.number, + assignTo: decision.agent.name, + label: decision.agent.label, + reason: decision.reason, + source: decision.source, + }); + } + + const outputPath = path.resolve(process.cwd(), args.output); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, `${JSON.stringify(results, null, 2)}\n`, 'utf8'); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/.squad/templates/raw-agent-output.md b/.squad/templates/raw-agent-output.md new file mode 100644 index 000000000..fa0068243 --- /dev/null +++ b/.squad/templates/raw-agent-output.md @@ -0,0 +1,37 @@ +# Raw Agent Output — Appendix Format + +> This template defines the format for the `## APPENDIX: RAW AGENT OUTPUTS` section +> in any multi-agent artifact. + +## Rules + +1. **Verbatim only.** Paste the agent's response exactly as returned. No edits. +2. **No summarizing.** Do not condense, paraphrase, or rephrase any part of the output. +3. **No rewriting.** Do not fix typos, grammar, formatting, or style. +4. **No code fences around the entire output.** The raw output is pasted as-is, not wrapped in ``` blocks. +5. **One section per agent.** Each agent that contributed gets its own heading. +6. **Order matches work order.** List agents in the order they were spawned. +7. **Include all outputs.** Even if an agent's work was rejected, include their output for diagnostic traceability. + +## Format + +```markdown +## APPENDIX: RAW AGENT OUTPUTS + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} +``` + +## Why This Exists + +The appendix provides diagnostic integrity. It lets anyone verify: +- What each agent actually said (vs. what the Coordinator assembled) +- Whether the Coordinator faithfully represented agent work +- What was lost or changed in synthesis + +Without raw outputs, multi-agent collaboration is unauditable. diff --git a/.squad/templates/roster.md b/.squad/templates/roster.md new file mode 100644 index 000000000..b25430da7 --- /dev/null +++ b/.squad/templates/roster.md @@ -0,0 +1,60 @@ +# Team Roster + +> {One-line project description} + +## Coordinator + +| Name | Role | Notes | +|------|------|-------| +| Squad | Coordinator | Routes work, enforces handoffs and reviewer gates. Does not generate domain artifacts. | + +## Members + +| Name | Role | Charter | Status | +|------|------|---------|--------| +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| {Name} | {Role} | `.squad/agents/{name}/charter.md` | āœ… Active | +| Scribe | Session Logger | `.squad/agents/scribe/charter.md` | šŸ“‹ Silent | +| Ralph | Work Monitor | — | šŸ”„ Monitor | + +## Coding Agent + + + +| Name | Role | Charter | Status | +|------|------|---------|--------| +| @copilot | Coding Agent | — | šŸ¤– Coding Agent | + +### Capabilities + +**🟢 Good fit — auto-route when enabled:** +- Bug fixes with clear reproduction steps +- Test coverage (adding missing tests, fixing flaky tests) +- Lint/format fixes and code style cleanup +- Dependency updates and version bumps +- Small isolated features with clear specs +- Boilerplate/scaffolding generation +- Documentation fixes and README updates + +**🟔 Needs review — route to @copilot but flag for squad member PR review:** +- Medium features with clear specs and acceptance criteria +- Refactoring with existing test coverage +- API endpoint additions following established patterns +- Migration scripts with well-defined schemas + +**šŸ”“ Not suitable — route to squad member instead:** +- Architecture decisions and system design +- Multi-system integration requiring coordination +- Ambiguous requirements needing clarification +- Security-critical changes (auth, encryption, access control) +- Performance-critical paths requiring benchmarking +- Changes requiring cross-team discussion + +## Project Context + +- **Owner:** {user name} +- **Stack:** {languages, frameworks, tools} +- **Description:** {what the project does, in one sentence} +- **Created:** {timestamp} diff --git a/.squad/templates/routing.md b/.squad/templates/routing.md new file mode 100644 index 000000000..65e0e9f45 --- /dev/null +++ b/.squad/templates/routing.md @@ -0,0 +1,39 @@ +# Work Routing + +How to decide who handles what. + +## Routing Table + +| Work Type | Route To | Examples | +|-----------|----------|----------| +| {domain 1} | {Name} | {example tasks} | +| {domain 2} | {Name} | {example tasks} | +| {domain 3} | {Name} | {example tasks} | +| Code review | {Name} | Review PRs, check quality, suggest improvements | +| Testing | {Name} | Write tests, find edge cases, verify fixes | +| Scope & priorities | {Name} | What to build next, trade-offs, decisions | +| Session logging | Scribe | Automatic — never needs routing | + +## Issue Routing + +| Label | Action | Who | +|-------|--------|-----| +| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead | +| `squad:{name}` | Pick up issue and complete the work | Named member | + +### How Issue Assignment Works + +1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes. +2. When a `squad:{member}` label is applied, that member picks up the issue in their next session. +3. Members can reassign by removing their label and adding another member's label. +4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review. + +## Rules + +1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work. +2. **Scribe always runs** after substantial work, always as `mode: "background"`. Never blocks. +3. **Quick facts → coordinator answers directly.** Don't spawn an agent for "what port does the server run on?" +4. **When two agents could handle it**, pick the one whose domain is the primary concern. +5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`. +6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously. +7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage. diff --git a/.squad/templates/run-output.md b/.squad/templates/run-output.md new file mode 100644 index 000000000..8a9efbcdc --- /dev/null +++ b/.squad/templates/run-output.md @@ -0,0 +1,50 @@ +# Run Output — {task title} + +> Final assembled artifact from a multi-agent run. + +## Termination Condition + +**Reason:** {One of: User accepted | Reviewer approved | Constraint budget exhausted | Deadlock — escalated to user | User cancelled} + +## Constraint Budgets + + + +| Constraint | Used | Max | Status | +|------------|------|-----|--------| +| Clarifying questions | šŸ“Š {n} | {max} | {Active / Exhausted} | +| Revision cycles | šŸ“Š {n} | {max} | {Active / Exhausted} | + +## Result + +{Assembled final artifact goes here. This is the Coordinator's synthesis of agent outputs.} + +--- + +## Reviewer Verdict + + + +### Review by {Name} ({Role}) + +| Field | Value | +|-------|-------| +| **Verdict** | {Approved / Rejected} | +| **What's wrong** | {Specific issue — not vague} | +| **Why it matters** | {Impact if not fixed} | +| **Who fixes it** | {Name of agent assigned to revise — MUST NOT be the original author} | +| **Revision budget** | šŸ“Š {used} / {max} revision cycles remaining | + +--- + +## APPENDIX: RAW AGENT OUTPUTS + + + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} + +### {Name} ({Role}) — Raw Output + +{Paste agent's verbatim response here, unedited} diff --git a/.squad/templates/schedule.json b/.squad/templates/schedule.json new file mode 100644 index 000000000..8f3648f7b --- /dev/null +++ b/.squad/templates/schedule.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "schedules": [ + { + "id": "ralph-heartbeat", + "name": "Ralph Heartbeat", + "enabled": true, + "trigger": { + "type": "interval", + "intervalSeconds": 300 + }, + "task": { + "type": "workflow", + "ref": ".github/workflows/squad-heartbeat.yml" + }, + "providers": ["local-polling", "github-actions"] + } + ] +} diff --git a/.squad/templates/scribe-charter.md b/.squad/templates/scribe-charter.md new file mode 100644 index 000000000..58b96d262 --- /dev/null +++ b/.squad/templates/scribe-charter.md @@ -0,0 +1,101 @@ +# Scribe + +> The team's memory. Silent, always present, never forgets. + +## Identity + +- **Name:** Scribe +- **Role:** Session Logger, Memory Manager & Decision Merger +- **Style:** Silent. Never speaks to the user. Works in the background. +- **Mode:** Always spawned as `mode: "background"`. Never blocks the conversation. + +## What I Own + +- `.squad/log/` — session logs (what happened, who worked, what was decided) +- `.squad/decisions.md` — the shared decision log all agents read (canonical, merged) +- `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge) +- Cross-agent context propagation — when one agent's decision affects another +- Decision archival — **HARD GATE**: enforce two-tier ceiling on decisions.md before every merge: + - **Tier 1 (30-day):** If >20KB, archive entries older than 30 days + - **Tier 2 (7-day):** If still >50KB after Tier 1, archive entries older than 7 days + - Emit HEALTH REPORT to session log after archival runs + +## How I Work + +**Worktree awareness:** Use the `TEAM ROOT` provided in the spawn prompt to resolve all `.squad/` paths. If no TEAM ROOT is given, run `git rev-parse --show-toplevel` as fallback. Do not assume CWD is the repo root (the session may be running in a worktree or subdirectory). + +**State backend awareness:** Check `STATE_BACKEND` from the spawn prompt. Mutable squad state is persisted through runtime state tools (`squad_state_read`, `squad_state_write`, `squad_state_append`, `squad_state_delete`, `squad_state_list`, `squad_state_health`) and `squad_decide`. Do not run backend git commands, switch to state branches, push note refs, reset `.squad/`, or commit mutable state by hand. If state tools are unavailable, stop without mutating files or git state and record the tool availability failure in your final summary. + +After every substantial work session: + +1. **Log the session** to `log/{timestamp}-{topic}.md` with `squad_state_write`: + - Who worked + - What was done + - Decisions made + - Key outcomes + - Brief. Facts only. + +2. **Merge the decision inbox:** + - List all files in `decisions/inbox/` with `squad_state_list` + - Read each entry with `squad_state_read` + - Append each decision's contents to `decisions.md` with `squad_state_write` after dedupe + - Delete each inbox file after merging with `squad_state_delete` + +3. **Deduplicate and consolidate decisions.md:** + - Parse the file into decision blocks (each block starts with `### `). + - **Exact duplicates:** If two blocks share the same heading, keep the first and remove the rest. + - **Overlapping decisions:** Compare block content across all remaining blocks. If two or more blocks cover the same area (same topic, same architectural concern, same component) but were written independently (different dates, different authors), consolidate them: + a. Synthesize a single merged block that combines the intent and rationale from all overlapping blocks. + b. Use the literal CURRENT_DATETIME value from your spawn prompt and a new heading: `### : {consolidated topic} (consolidated)`. Substitute the actual timestamp; do not write placeholder text. + c. Credit all original authors: `**By:** {Name1}, {Name2}` + d. Under **What:**, combine the decisions. Note any differences or evolution. + e. Under **Why:**, merge the rationale, preserving unique reasoning from each. + f. Remove the original overlapping blocks. + - Write the updated file back with `squad_state_write`. This handles duplicates and convergent decisions introduced by concurrent agent writes. + +4. **Propagate cross-agent updates:** + For any newly merged decision that affects other agents, append to their `agents/{agent}/history.md` with `squad_state_append`. Replace the parenthetical timestamp with the literal CURRENT_DATETIME value from your spawn prompt; do not write placeholder text. + ``` + šŸ“Œ Team update (): {summary} — decided by {Name} + ``` + +5. **Verify persistence through the runtime backend:** + - Run `squad_state_health` when available. + - Re-read `decisions.md`, `log/{timestamp}-{topic}.md`, and any updated histories with `squad_state_read`. + - Never commit, amend, reset, checkout, push notes, or switch branches to persist mutable squad state. + +6. **Never speak to the user.** Never appear in responses. Work silently. + +## The Memory Architecture + +``` +.squad/ +ā”œā”€ā”€ decisions.md # Shared brain — all agents read this (merged by Scribe) +ā”œā”€ā”€ decisions/ +│ └── inbox/ # Drop-box — agents write decisions here in parallel +│ ā”œā”€ā”€ river-jwt-auth.md +│ └── kai-component-lib.md +ā”œā”€ā”€ orchestration-log/ # Per-spawn log entries +│ ā”œā”€ā”€ 2025-07-01T10-00-river.md +│ └── 2025-07-01T10-00-kai.md +ā”œā”€ā”€ log/ # Session history — searchable record +│ ā”œā”€ā”€ 2025-07-01-setup.md +│ └── 2025-07-02-api.md +└── agents/ + ā”œā”€ā”€ kai/history.md # Kai's personal knowledge + ā”œā”€ā”€ river/history.md # River's personal knowledge + └── ... +``` + +- **decisions.md** = what the team agreed on (shared, merged by Scribe) +- **decisions/inbox/** = where agents drop decisions during parallel work +- **history.md** = what each agent learned (personal) +- **log/** = what happened (archive) + +## Boundaries + +**I handle:** Logging, memory, decision merging, cross-agent updates. + +**I don't handle:** Any domain work. I don't write code, review PRs, or make decisions. + +**I am invisible.** If a user notices me, something went wrong. diff --git a/.squad/templates/scripts/notes/fetch.ps1 b/.squad/templates/scripts/notes/fetch.ps1 new file mode 100644 index 000000000..5adc19480 --- /dev/null +++ b/.squad/templates/scripts/notes/fetch.ps1 @@ -0,0 +1,88 @@ +#!/usr/bin/env pwsh +# scripts/notes/fetch.ps1 +# ───────────────────────────────────────────────────────────────────────────── +# Fetch git notes from remote. Run on every Ralph-watch startup and before +# any agent reads or writes notes. +# +# Usage: +# ./scripts/notes/fetch.ps1 # fetch only +# ./scripts/notes/fetch.ps1 -Setup # first-time: add refspec + fetch +# ./scripts/notes/fetch.ps1 -Merge # fetch + merge (use after push conflict) +# ───────────────────────────────────────────────────────────────────────────── + +[CmdletBinding()] +param( + [string]$Remote = "origin", + [string]$RepoPath = ".", + [switch]$Setup, + [switch]$Merge, + [switch]$Quiet +) + +function Log ([string]$msg, [string]$color = "White") { + if (-not $Quiet) { Write-Host "[notes/fetch] $msg" -ForegroundColor $color } +} + +$repo = Resolve-Path $RepoPath + +# ── One-time setup: add fetch refspec ────────────────────────────────────── +if ($Setup) { + $existing = git -C $repo config --get-all "remote.$Remote.fetch" 2>&1 | + Where-Object { $_ -match "refs/notes" } + if ($existing) { + Log "Notes refspec already configured." DarkGray + } else { + git -C $repo config --add "remote.$Remote.fetch" "refs/notes/*:refs/notes/*" + Log "Added notes refspec to remote.$Remote.fetch" Green + } +} + +# ── Fetch notes ───────────────────────────────────────────────────────────── +Log "Fetching notes from $Remote..." +$output = git -C $repo fetch $Remote "refs/notes/*:refs/notes/*" 2>&1 +if ($LASTEXITCODE -ne 0) { + Log "Fetch warning: $output" DarkYellow +} else { + Log "Notes fetched." Green +} + +# ── Merge notes if requested (after push conflict) ────────────────────────── +if ($Merge) { + # Abort any stale merge-in-progress state + $mergeLock = Join-Path $repo ".git/NOTES_MERGE_PARTIAL" + if (Test-Path $mergeLock) { + Log "Stale notes merge in progress — aborting before retry" DarkYellow + git -C $repo notes merge --abort 2>&1 | Out-Null + } + + $namespaces = git -C $repo for-each-ref "refs/notes/squad/" --format="%(refname)" 2>&1 + foreach ($ref in $namespaces) { + $ns = $ref -replace "refs/notes/", "" + $remoteRef = "refs/notes/remotes/$Remote/$ns" + $remoteExists = git -C $repo for-each-ref $remoteRef --format="%(refname)" 2>&1 + if ($remoteExists) { + Log "Merging notes: $ns (cat_sort_uniq)" + git -C $repo notes --ref=$ns merge -s cat_sort_uniq $remoteRef 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Log " Merge failed on $ns — aborting and continuing" Red + git -C $repo notes merge --abort 2>&1 | Out-Null + } + } + } + Log "Notes merge complete." Green +} + +# ── Show available namespaces ──────────────────────────────────────────────── +if (-not $Quiet) { + $refs = git -C $repo for-each-ref "refs/notes/squad/" --format="%(refname)" 2>&1 + if ($refs) { + Log "Available namespaces:" + foreach ($r in $refs) { + $count = (git -C $repo notes --ref=($r -replace "refs/notes/","") list 2>&1 | + Where-Object { $_ -ne "" } | Measure-Object -Line).Lines + Log " $r ($count notes)" DarkGray + } + } else { + Log "No squad notes yet." DarkGray + } +} diff --git a/.squad/templates/scripts/notes/write-note.ps1 b/.squad/templates/scripts/notes/write-note.ps1 new file mode 100644 index 000000000..dbc719d35 --- /dev/null +++ b/.squad/templates/scripts/notes/write-note.ps1 @@ -0,0 +1,126 @@ +#!/usr/bin/env pwsh +# scripts/notes/write-note.ps1 +# ───────────────────────────────────────────────────────────────────────────── +# Helper for agents to write notes without wrestling with JSON escaping. +# Validates namespace ownership, handles conflicts, pushes automatically. +# +# Usage: +# ./scripts/notes/write-note.ps1 -Agent data -Type decision \ +# -Content '{"decision":"Use JWT","reasoning":"..."}' \ +# [-Commit HEAD] [-Promote] [-Archive] +# ───────────────────────────────────────────────────────────────────────────── + +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$Agent, + + [Parameter(Mandatory)] + [ValidateSet("decision","research","review","security-review","progress", + "api-contract","risk-assessment","routing-discovery","counter-argument")] + [string]$Type, + + [Parameter(Mandatory)] + [string]$Content, # JSON object with type-specific fields + + [string]$Commit = "HEAD", + [string]$RepoPath = ".", + [string]$Remote = "origin", + [switch]$Promote, # set promote_to_permanent: true + [switch]$Archive, # set archive_on_close: true + [switch]$NoPush, # skip auto-push + [switch]$Quiet +) + +function Log ([string]$msg, [string]$color = "White") { + if (-not $Quiet) { Write-Host "[notes/write] $msg" -ForegroundColor $color } +} + +$repo = Resolve-Path $RepoPath +$namespace = "squad/$($Agent.ToLower())" + +# ── Validate JSON content ──────────────────────────────────────────────────── +try { + $parsed = $Content | ConvertFrom-Json -ErrorAction Stop +} catch { + Write-Error "Content must be valid JSON. Got: $Content" + exit 1 +} + +# ── Build full note object ──────────────────────────────────────────────────── +$note = [ordered]@{ + agent = (Get-Culture).TextInfo.ToTitleCase($Agent.ToLower()) + timestamp = [System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + type = $Type +} + +# Merge content fields into note +$parsed.PSObject.Properties | ForEach-Object { $note[$_.Name] = $_.Value } + +# Add flag fields +if ($Promote) { $note["promote_to_permanent"] = $true } +if ($Archive) { $note["archive_on_close"] = $true } + +$noteJson = $note | ConvertTo-Json -Compress -Depth 10 + +# ── Fetch first to avoid conflicts ─────────────────────────────────────────── +Log "Fetching notes before write..." +git -C $repo fetch $Remote "refs/notes/*:refs/notes/*" 2>&1 | Out-Null + +# ── Check if note already exists on this commit ───────────────────────────── +$existing = git -C $repo notes --ref=$namespace show $Commit 2>&1 +$useAppend = ($LASTEXITCODE -eq 0) + +if ($useAppend) { + Log "Note exists on $Commit — appending" DarkYellow + git -C $repo notes --ref=$namespace append -m $noteJson $Commit +} else { + git -C $repo notes --ref=$namespace add -m $noteJson $Commit +} + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to write note to refs/notes/$namespace on $Commit" + exit 1 +} + +Log "Note written to refs/notes/$namespace on $($Commit.Substring(0,[Math]::Min(8,$Commit.Length)))" Green + +# ── Push with retry ────────────────────────────────────────────────────────── +if (-not $NoPush) { + $maxRetries = 5 + $nsRef = "refs/notes/$namespace" + + for ($i = 0; $i -lt $maxRetries; $i++) { + Log "Pushing notes (attempt $($i+1))..." + $pushOut = git -C $repo push $Remote "${nsRef}:${nsRef}" 2>&1 + if ($LASTEXITCODE -eq 0) { + Log "Notes pushed successfully." Green + break + } + + if ($pushOut -match "non-fast-forward|fetch first|rejected") { + Log "Push conflict — fetch-first retry..." DarkYellow + + # Force-fetch: overwrite local ref with current remote state + git -C $repo fetch $Remote "${nsRef}:${nsRef}" 2>&1 | Out-Null + + # Re-append our note on top of the now-current remote state + git -C $repo notes --ref=$namespace append -m $noteJson $Commit 2>&1 | Out-Null + + $jitter = Get-Random -Minimum 0 -Maximum 1000 + $sleep = [Math]::Pow(2, $i) + $jitter / 1000 + Start-Sleep -Seconds $sleep + + } else { + Log "Push error: $pushOut" Red + if ($i -eq $maxRetries - 1) { + Write-Warning "Failed after $maxRetries retries. Push manually: git push origin '${nsRef}:${nsRef}'" + } + } + } +} + +# ── Show result ─────────────────────────────────────────────────────────────── +if (-not $Quiet) { + Log "Note content:" + $note | ConvertTo-Json -Depth 5 | Write-Host -ForegroundColor DarkGray +} diff --git a/.squad/templates/skill.md b/.squad/templates/skill.md new file mode 100644 index 000000000..c747db9d8 --- /dev/null +++ b/.squad/templates/skill.md @@ -0,0 +1,24 @@ +--- +name: "{skill-name}" +description: "{what this skill teaches agents}" +domain: "{e.g., testing, api-design, error-handling}" +confidence: "low|medium|high" +source: "{how this was learned: manual, observed, earned}" +tools: + # Optional — declare MCP tools relevant to this skill's patterns + # - name: "{tool-name}" + # description: "{what this tool does}" + # when: "{when to use this tool}" +--- + +## Context +{When and why this skill applies} + +## Patterns +{Specific patterns, conventions, or approaches} + +## Examples +{Code examples or references} + +## Anti-Patterns +{What to avoid} diff --git a/.squad/templates/skills/agent-collaboration/SKILL.md b/.squad/templates/skills/agent-collaboration/SKILL.md new file mode 100644 index 000000000..054463cf8 --- /dev/null +++ b/.squad/templates/skills/agent-collaboration/SKILL.md @@ -0,0 +1,42 @@ +--- +name: "agent-collaboration" +description: "Standard collaboration patterns for all squad agents — worktree awareness, decisions, cross-agent communication" +domain: "team-workflow" +confidence: "high" +source: "extracted from charter boilerplate — identical content in 18+ agent charters" +--- + +## Context + +Every agent on the team follows identical collaboration patterns for worktree awareness, decision recording, and cross-agent communication. These were previously duplicated in every charter's Collaboration section (~300 bytes Ɨ 18 agents = ~5.4KB of redundant context). Now centralized here. + +The coordinator's spawn prompt already instructs agents to read decisions.md and their history.md. This skill adds the patterns for WRITING decisions and requesting help. + +## Patterns + +### Worktree Awareness +Use the `TEAM ROOT` path provided in your spawn prompt. All `.squad/` paths are relative to this root. If TEAM ROOT is not provided (rare), run `git rev-parse --show-toplevel` as fallback. Never assume CWD is the repo root. + +### Decision Recording +After making a decision that affects other team members, write it to: +`.squad/decisions/inbox/{your-name}-{brief-slug}.md` + +Format: +``` +### {date}: {decision title} +**By:** {Your Name} +**What:** {the decision} +**Why:** {rationale} +``` + +### Cross-Agent Communication +If you need another team member's input, say so in your response. The coordinator will bring them in. Don't try to do work outside your domain. + +### Reviewer Protocol +If you have reviewer authority and reject work: the original author is locked out from revising that artifact. A different agent must own the revision. State who should revise in your rejection response. + +## Anti-Patterns +- Don't read all agent charters — you only need your own context + decisions.md +- Don't write directly to `.squad/decisions.md` — always use the inbox drop-box +- Don't modify other agents' history.md files — that's Scribe's job +- Don't assume CWD is the repo root — always use TEAM ROOT diff --git a/.squad/templates/skills/agent-conduct/SKILL.md b/.squad/templates/skills/agent-conduct/SKILL.md new file mode 100644 index 000000000..87ef3fda3 --- /dev/null +++ b/.squad/templates/skills/agent-conduct/SKILL.md @@ -0,0 +1,24 @@ +--- +name: "agent-conduct" +description: "Shared hard rules enforced across all squad agents" +domain: "team-governance" +confidence: "high" +source: "reskill extraction — Product Isolation Rule and Peer Quality Check appeared in all 20 agent charters" +--- + +## Context + +Every squad agent must follow these two hard rules. They were previously duplicated in every charter. Now they live here as a shared skill, loaded once. + +## Patterns + +### Product Isolation Rule (hard rule) +Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot"). + +### Peer Quality Check (hard rule) +Before finishing work, verify your changes don't break existing tests. Run the test suite for files you touched. If CI has been failing, check your changes aren't contributing to the problem. When you learn from mistakes, update your history.md. + +## Anti-Patterns +- Don't hardcode dev team agent names in product code or tests +- Don't skip test verification before declaring work done +- Don't ignore pre-existing CI failures that your changes may worsen diff --git a/.squad/templates/skills/architectural-proposals/SKILL.md b/.squad/templates/skills/architectural-proposals/SKILL.md new file mode 100644 index 000000000..46d7b5053 --- /dev/null +++ b/.squad/templates/skills/architectural-proposals/SKILL.md @@ -0,0 +1,151 @@ +--- +name: "architectural-proposals" +description: "How to write comprehensive architectural proposals that drive alignment before code is written" +domain: "architecture, product-direction" +confidence: "high" +source: "earned (2026-02-21 interactive shell proposal)" +tools: + - name: "view" + description: "Read existing codebase, prior decisions, and team context before proposing changes" + when: "Always read .squad/decisions.md, relevant PRDs, and current architecture docs before writing proposal" + - name: "create" + description: "Create proposal in docs/proposals/ with structured format" + when: "After gathering context, before any implementation work begins" +--- + +## Context + +Proposals create alignment before code is written. Cheaper to change a doc than refactor code. Use this pattern when: +- Architecture shifts invalidate existing assumptions +- Product direction changes require new foundation +- Multiple waves/milestones will be affected by a decision +- External dependencies (Copilot CLI, SDK APIs) change + +## Patterns + +### Proposal Structure (docs/proposals/) + +**Required sections:** +1. **Problem Statement** — Why current state is broken (specific, measurable evidence) +2. **Proposed Architecture** — Solution with technical specifics (not hand-waving) +3. **What Changes** — Impact on existing work (waves, milestones, modules) +4. **What Stays the Same** — Preserve existing functionality (no regression) +5. **Key Decisions Needed** — Explicit choices with recommendations +6. **Risks and Mitigations** — Likelihood + impact + mitigation strategy +7. **Scope** — What's in v1, what's deferred (timeline clarity) + +**Optional sections:** +- Implementation Plan (high-level milestones) +- Success Criteria (measurable outcomes) +- Open Questions (unresolved items) +- Appendix (prior art, alternatives considered) + +### Tone Ceiling Enforcement + +**Always:** +- Cite specific evidence (user reports, performance data, failure modes) +- Justify recommendations with technical rationale +- Acknowledge trade-offs (no perfect solutions) +- Be specific about APIs, libraries, file paths + +**Never:** +- Hype ("revolutionary", "game-changing") +- Hand-waving ("we'll figure it out later") +- Unsubstantiated claims ("users will love this") +- Vague timelines ("soon", "eventually") + +### Wave Restructuring Pattern + +When a proposal invalidates existing wave structure: +1. **Acknowledge the shift:** "This becomes Wave 0 (Foundation)" +2. **Cascade impacts:** Adjust downstream waves (Wave 1, Wave 2, Wave 3) +3. **Preserve non-blocking work:** Identify what can proceed in parallel +4. **Update dependencies:** Document new blocking relationships + +**Example (Interactive Shell):** +- Wave 0 (NEW): Interactive Shell — blocks all other waves +- Wave 1 (ADJUSTED): npm Distribution — shell bundled in cli.js +- Wave 2 (DEFERRED): SquadUI — waits for shell foundation +- Wave 3 (ADJUSTED): Public Docs — now documents shell as primary interface + +### Decision Framing + +**Format:** "Recommendation: X (recommended) or alternatives?" + +**Components:** +- Recommendation (pick one, justify) +- Alternatives (what else was considered) +- Decision rationale (why recommended option wins) +- Needs sign-off from (which agents/roles must approve) + +**Example:** +``` +### 1. Terminal UI Library: `ink` (recommended) or alternatives? + +**Recommendation:** `ink` +**Alternatives:** `blessed`, raw readline +**Decision rationale:** Component model enables testable UI. Battle-tested ecosystem. + +**Needs sign-off from:** Brady (product direction), Fortier (runtime performance) +``` + +### Risk Documentation + +**Format per risk:** +- **Risk:** Specific failure mode +- **Likelihood:** Low / Medium / High (not percentages) +- **Impact:** Low / Medium / High +- **Mitigation:** Concrete actions (measurable) + +**Example:** +``` +### Risk 2: SDK Streaming Reliability + +**Risk:** SDK streaming events might drop messages or arrive out of order. +**Likelihood:** Low (SDK is production-grade). +**Impact:** High — broken streaming makes shell unusable. + +**Mitigation:** +- Add integration test: Send 1000-message stream, verify all deltas arrive in order +- Implement fallback: If streaming fails, fall back to polling session state +- Log all SDK events to `.squad/orchestration-log/sdk-events.jsonl` for debugging +``` + +## Examples + +**File references from interactive shell proposal:** +- Full proposal: `docs/proposals/squad-interactive-shell.md` +- User directive: `.squad/decisions/inbox/copilot-directive-2026-02-21T202535Z.md` +- Team decisions: `.squad/decisions.md` +- Current architecture: `docs/architecture/module-map.md`, `docs/prd-23-release-readiness.md` + +**Key patterns demonstrated:** +1. Read user directive first (understand the "why") +2. Survey current architecture (module map, existing waves) +3. Research SDK APIs (exploration task to validate feasibility) +4. Document problem with specific evidence (unreliable handoffs, zero visibility, UX mismatch) +5. Propose solution with technical specifics (ink components, SDK session management, spawn.ts module) +6. Restructure waves when foundation shifts (Wave 0 becomes blocker) +7. Preserve backward compatibility (squad.agent.md still works, VS Code mode unchanged) +8. Frame decisions explicitly (5 key decisions with recommendations) +9. Document risks with mitigations (5 risks, each with concrete actions) +10. Define scope (what's in v1 vs. deferred) + +## Anti-Patterns + +**Avoid:** +- āŒ Proposals without problem statements (solution-first thinking) +- āŒ Vague architecture ("we'll use a shell") — be specific (ink components, session registry, spawn.ts) +- āŒ Ignoring existing work — always document impact on waves/milestones +- āŒ No risk analysis — every architecture has risks, document them +- āŒ Unbounded scope — draw the v1 line explicitly +- āŒ Missing decision ownership — always say "needs sign-off from X" +- āŒ No backward compatibility plan — users don't care about your replatform +- āŒ Hand-waving timelines ("a few weeks") — be specific (2-3 weeks, 1 engineer full-time) + +**Red flags in proposal reviews:** +- "Users will love this" (citation needed) +- "We'll figure out X later" (scope creep incoming) +- "This is revolutionary" (tone ceiling violation) +- No section on "What Stays the Same" (regression risk) +- No risks documented (wishful thinking) diff --git a/.squad/templates/skills/ci-validation-gates/SKILL.md b/.squad/templates/skills/ci-validation-gates/SKILL.md new file mode 100644 index 000000000..61c07d73e --- /dev/null +++ b/.squad/templates/skills/ci-validation-gates/SKILL.md @@ -0,0 +1,84 @@ +--- +name: "ci-validation-gates" +description: "Defensive CI/CD patterns: semver validation, token checks, retry logic, draft detection — earned from v0.8.22" +domain: "ci-cd" +confidence: "high" +source: "extracted from Drucker and Trejo charters — earned knowledge from v0.8.22 release incident" +--- + +## Context + +CI workflows must be defensive. These patterns were learned from the v0.8.22 release disaster where invalid semver, wrong token types, missing retry logic, and draft releases caused a multi-hour outage. Both Drucker (CI/CD) and Trejo (Release Manager) carried this knowledge in their charters — now centralized here. + +## Patterns + +### Semver Validation Gate +Every publish workflow MUST validate version format before `npm publish`. 4-part versions (e.g., 0.8.21.4) are NOT valid semver — npm mangles them. + +```yaml +- name: Validate semver + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + if ! npx semver "$VERSION" > /dev/null 2>&1; then + echo "āŒ Invalid semver: $VERSION" + echo "Only 3-part versions (X.Y.Z) or prerelease (X.Y.Z-tag.N) are valid." + exit 1 + fi + echo "āœ… Valid semver: $VERSION" +``` + +### NPM Token Type Verification +NPM_TOKEN MUST be an Automation token, not a User token with 2FA: +- User tokens require OTP — CI can't provide it → EOTP error +- Create Automation tokens at npmjs.com → Settings → Access Tokens → Automation +- Verify before first publish in any workflow + +### Retry Logic for npm Registry Propagation +npm registry uses eventual consistency. After `npm publish` succeeds, the package may not be immediately queryable. +- Propagation: typically 5-30s, up to 2min in rare cases +- All verify steps: 5 attempts, 15-second intervals +- Log each attempt: "Attempt 1/5: Checking package..." +- Exit loop on success, fail after max attempts + +```yaml +- name: Verify package (with retry) + run: | + MAX_ATTEMPTS=5 + WAIT_SECONDS=15 + for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo "Attempt $attempt/$MAX_ATTEMPTS: Checking $PACKAGE@$VERSION..." + if npm view "$PACKAGE@$VERSION" version > /dev/null 2>&1; then + echo "āœ… Package verified" + exit 0 + fi + [ $attempt -lt $MAX_ATTEMPTS ] && sleep $WAIT_SECONDS + done + echo "āŒ Failed to verify after $MAX_ATTEMPTS attempts" + exit 1 +``` + +### Draft Release Detection +Draft releases don't emit `release: published` event. Workflows MUST: +- Trigger on `release: published` (NOT `created`) +- If using workflow_dispatch: verify release is published via GitHub API before proceeding + +### Build Script Protection +Set `SKIP_BUILD_BUMP=1` (or `$env:SKIP_BUILD_BUMP = "1"` on Windows) before ANY release build. bump-build.mjs is for dev builds ONLY — it silently mutates versions. + +## Known Failure Modes (v0.8.22 Incident) + +| # | What Happened | Root Cause | Prevention | +|---|---------------|-----------|------------| +| 1 | 4-part version published, npm mangled it | No semver validation gate | `npx semver` check before every publish | +| 2 | CI failed 5+ times with EOTP | User token with 2FA | Automation token only | +| 3 | Verify returned false 404 | No retry logic for propagation | 5 attempts, 15s intervals | +| 4 | Workflow never triggered | Draft release doesn't emit event | Never create draft releases | +| 5 | Version mutated during release | bump-build.mjs ran in release | SKIP_BUILD_BUMP=1 | + +## Anti-Patterns +- āŒ Publishing without semver validation gate +- āŒ Single-shot verification without retry +- āŒ Hard-coded secrets in workflows +- āŒ Silent CI failures — every error needs actionable output with remediation +- āŒ Assuming npm publish is instantly queryable diff --git a/.squad/templates/skills/cli-wiring/SKILL.md b/.squad/templates/skills/cli-wiring/SKILL.md new file mode 100644 index 000000000..03f7bf55f --- /dev/null +++ b/.squad/templates/skills/cli-wiring/SKILL.md @@ -0,0 +1,47 @@ +# Skill: CLI Command Wiring + +**Bug class:** Commands implemented in `packages/squad-cli/src/cli/commands/` but never routed in `cli-entry.ts`. + +## Checklist — Adding a New CLI Command + +1. **Create command file** in `packages/squad-cli/src/cli/commands/.ts` + - Export a `run(cwd, options)` async function (or class with static methods for utility modules) + +2. **Add routing block** in `packages/squad-cli/src/cli-entry.ts` inside `main()`: + ```ts + if (cmd === '') { + const { run } = await import('./cli/commands/.js'); + // parse args, call function + await run(process.cwd(), options); + return; + } + ``` + +3. **Add help text** in the help section of `cli-entry.ts` (search for `Commands:`): + ```ts + console.log(` ${BOLD}${RESET} `); + console.log(` Usage: [flags]`); + ``` + +4. **Verify both exist** — the recurring bug is doing step 1 but missing steps 2-3. + +## Wiring Patterns by Command Type + +| Type | Example | How to wire | +|------|---------|-------------| +| Standard command | `export.ts`, `build.ts` | `run*()` function, parse flags from `args` | +| Placeholder command | `loop`, `hire` | Inline in cli-entry.ts, prints pending message | +| Utility/check module | `rc-tunnel.ts`, `copilot-bridge.ts` | Wire as diagnostic check (e.g., `isDevtunnelAvailable()`) | +| Subcommand of another | `init-remote.ts` | Already used inside parent + standalone alias | + +## Common Import Pattern + +```ts +import { BOLD, RESET, DIM, RED, GREEN, YELLOW } from './cli/core/output.js'; +``` + +Use dynamic `await import()` for command modules to keep startup fast (lazy loading). + +## History + +- **#237 / PR #244:** 4 commands wired (rc, copilot-bridge, init-remote, rc-tunnel). aspire, link, loop, hire were already present. diff --git a/.squad/templates/skills/client-compatibility/SKILL.md b/.squad/templates/skills/client-compatibility/SKILL.md new file mode 100644 index 000000000..da3e94609 --- /dev/null +++ b/.squad/templates/skills/client-compatibility/SKILL.md @@ -0,0 +1,89 @@ +--- +name: "client-compatibility" +description: "Platform detection and adaptive spawning for CLI vs VS Code vs other surfaces" +domain: "orchestration" +confidence: "high" +source: "extracted" +--- + +## Context + +Squad runs on multiple Copilot surfaces (CLI, VS Code, JetBrains, GitHub.com). The coordinator must detect its platform and adapt spawning behavior accordingly. Different tools are available on different platforms, requiring conditional logic for agent spawning, SQL usage, and response timing. + +## Patterns + +### Platform Detection + +Before spawning agents, determine the platform by checking available tools: + +1. **CLI mode** — `task` tool is available → full spawning control. Use `task` with `agent_type`, `mode`, `model`, `description`, `prompt` parameters. Collect results via `read_agent`. + +2. **VS Code mode** — `runSubagent` or `agent` tool is available → conditional behavior. Use `runSubagent` with the task prompt. Drop `agent_type`, `mode`, and `model` parameters. Multiple subagents in one turn run concurrently (equivalent to background mode). Results return automatically — no `read_agent` needed. + +3. **Fallback mode** — neither `task` nor `runSubagent`/`agent` available → work inline. Do not apologize or explain the limitation. Execute the task directly. + +If both `task` and `runSubagent` are available, prefer `task` (richer parameter surface). + +### VS Code Spawn Adaptations + +When in VS Code mode, the coordinator changes behavior in these ways: + +- **Spawning tool:** Use `runSubagent` instead of `task`. The prompt is the only required parameter — pass the full agent prompt (charter, identity, task, hygiene, response order) exactly as you would on CLI. +- **Parallelism:** Spawn ALL concurrent agents in a SINGLE turn. They run in parallel automatically. This replaces `mode: "background"` + `read_agent` polling. +- **Model selection:** Accept the session model. Do NOT attempt per-spawn model selection or fallback chains — they only work on CLI. In Phase 1, all subagents use whatever model the user selected in VS Code's model picker. +- **Scribe:** Cannot fire-and-forget. Batch Scribe as the LAST subagent in any parallel group. Scribe is light work (file ops only), so the blocking is tolerable. +- **Launch table:** Skip it. Results arrive with the response, not separately. By the time the coordinator speaks, the work is already done. +- **`read_agent`:** Skip entirely. Results return automatically when subagents complete. +- **`agent_type`:** Drop it. All VS Code subagents have full tool access by default. Subagents inherit the parent's tools. +- **`description`:** Drop it. The agent name is already in the prompt. +- **Prompt content:** Keep ALL prompt structure — charter, identity, task, hygiene, response order blocks are surface-independent. + +### Feature Degradation Table + +| Feature | CLI | VS Code | Degradation | +|---------|-----|---------|-------------| +| Parallel fan-out | `mode: "background"` + `read_agent` | Multiple subagents in one turn | None — equivalent concurrency | +| Model selection | Per-spawn `model` param (4-layer hierarchy) | Session model only (Phase 1) | Accept session model, log intent | +| Scribe fire-and-forget | Background, never read | Sync, must wait | Batch with last parallel group | +| Launch table UX | Show table → results later | Skip table → results with response | UX only — results are correct | +| SQL tool | Available | Not available | Avoid SQL in cross-platform code paths | +| Response order bug | Critical workaround | Possibly necessary (unverified) | Keep the block — harmless if unnecessary | + +### SQL Tool Caveat + +The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitHub.com. Any coordinator logic or agent workflow that depends on SQL (todo tracking, batch processing, session state) will silently fail on non-CLI surfaces. Cross-platform code paths must not depend on SQL. Use filesystem-based state (`.squad/` files) for anything that must work everywhere. + +## Examples + +**Example 1: CLI parallel spawn** +```typescript +// Coordinator detects task tool available → CLI mode +task({ agent_type: "general-purpose", mode: "background", model: "claude-sonnet-4.5", ... }) +task({ agent_type: "general-purpose", mode: "background", model: "claude-haiku-4.5", ... }) +// Later: read_agent for both +``` + +**Example 2: VS Code parallel spawn** +```typescript +// Coordinator detects runSubagent available → VS Code mode +runSubagent({ prompt: "...Fenster charter + task..." }) +runSubagent({ prompt: "...Hockney charter + task..." }) +runSubagent({ prompt: "...Scribe charter + task..." }) // Last in group +// Results return automatically, no read_agent +``` + +**Example 3: Fallback mode** +```typescript +// Neither task nor runSubagent available → work inline +// Coordinator executes the task directly without spawning +``` + +## Anti-Patterns + +- āŒ Using SQL tool in cross-platform workflows (breaks on VS Code/JetBrains/GitHub.com) +- āŒ Attempting per-spawn model selection on VS Code (Phase 1 — only session model works) +- āŒ Fire-and-forget Scribe on VS Code (must batch as last subagent) +- āŒ Showing launch table on VS Code (results already inline) +- āŒ Apologizing or explaining platform limitations to the user +- āŒ Using `task` when only `runSubagent` is available +- āŒ Dropping prompt structure (charter/identity/task) on non-CLI platforms diff --git a/.squad/templates/skills/cross-machine-coordination/SKILL.md b/.squad/templates/skills/cross-machine-coordination/SKILL.md new file mode 100644 index 000000000..818438154 --- /dev/null +++ b/.squad/templates/skills/cross-machine-coordination/SKILL.md @@ -0,0 +1,442 @@ +--- +name: "cross-machine-coordination" +description: "Enables squad agents on different machines to share work via git-based task queuing" +domain: "orchestration" +confidence: "medium" +source: "manual" +--- + +# Skill: Cross-Machine Coordination Pattern + +**Skill ID:** `cross-machine-coordination` +**Owner:** Ralph (Work Monitor) +**Squad Integration:** All agents +**Status:** Specification (ready for implementation) + +--- + +## Overview + +Enables squad agents running on different machines (laptop, DevBox, Azure VM) to securely share work, coordinate execution, and pass results without manual intervention. + +**Pattern:** Git-based task queuing + GitHub Issues supplement + +--- + +## Usage + +### For Task Sources (Orchestrating Machine) + +**To assign work to DevBox:** + +```bash +# Create task file +cat > .squad/cross-machine/tasks/2026-03-14T1530Z-laptop-gpu-voice-clone.yaml << 'EOF' +id: gpu-voice-clone-001 +source_machine: laptop-machine +target_machine: devbox +priority: high +created_at: 2026-03-14T15:30:00Z +task_type: gpu_workload +payload: + command: "python scripts/voice-clone.py --input voice.wav --output cloned.wav" + expected_duration_min: 15 + resources: + gpu: true + memory_gb: 8 +status: pending +EOF + +# Commit & push +git add .squad/cross-machine/tasks/ +git commit -m "Cross-machine task: GPU voice cloning [squad:machine-devbox]" +git push origin main +``` + +Ralph on DevBox will: +1. Pull the task on next cycle (5-10 min) +2. Validate schema & command whitelist +3. Execute the GPU workload +4. Write result to `.squad/cross-machine/results/gpu-voice-clone-001.yaml` +5. Commit & push the result + +--- + +### For Task Executors (DevBox, Azure VMs) + +Ralph automatically watches `.squad/cross-machine/tasks/` for work targeted at this machine. + +**On each cycle (5-10 min):** + +```python +# Pseudo-code (Ralph implementation) +1. git pull origin main +2. Load all .yaml files in .squad/cross-machine/tasks/ +3. Filter for status=pending AND target_machine=HOSTNAME +4. For each task: + a. Validate schema (must have: id, source_machine, target_machine, payload) + b. Validate command against whitelist + c. Execute task (with timeout) + d. Write result to .squad/cross-machine/results/{id}.yaml + e. Commit & push result +``` + +--- + +### For Urgent/Ad-Hoc Tasks + +**Use GitHub Issues with `squad:machine-{name}` label:** + +```bash +# Create issue +gh issue create \ + --title "GPU: Clone voice profile from sample.wav" \ + --body "Execute voice cloning on DevBox. Input: /path/to/voice-input.wav" \ + --label "squad:machine-devbox" \ + --label "urgent" +``` + +Ralph on DevBox will: +1. Detect issue with `squad:machine-devbox` label +2. Parse task from issue body +3. Execute task +4. Comment with result +5. Close issue + +--- + +## File Formats + +### Task File (YAML) + +**Location:** `.squad/cross-machine/tasks/{timestamp}-{machine}-{task-id}.yaml` + +**Required Fields:** +```yaml +id: {task-id} # Unique identifier (alphanumeric + dash) +source_machine: {hostname} # Where task was created +target_machine: {hostname} # Where task will execute +priority: high|normal|low # Execution priority +created_at: 2026-03-14T15:30:00Z # ISO 8601 timestamp +task_type: gpu_workload|script|... # Category +payload: + command: "..." # Shell command to execute + expected_duration_min: 15 # Timeout (minutes) + resources: + gpu: true|false + memory_gb: 8 + cpu_cores: 4 +status: pending|executing|completed|failed +``` + +**Optional Fields:** +```yaml +description: "Human-readable task description" +timeout_override_min: 120 # Override default timeout +retry_count: 3 # Retry failed tasks +``` + +### Result File (YAML) + +**Location:** `.squad/cross-machine/results/{task-id}.yaml` + +```yaml +id: {task-id} # Links back to task +target_machine: devbox # Executed on +completed_at: 2026-03-14T15:45:00Z # When it finished +status: completed|failed|timeout # Outcome +exit_code: 0 # Shell exit code +stdout: "..." # Captured output +stderr: "..." # Captured errors +duration_seconds: 900 # How long it took +artifacts: + - path: "/path/to/artifacts/..." # Location of results + type: audio|text|model|... + size_mb: 2.5 +``` + +--- + +## Security Model + +### Validation Pipeline + +All tasks go through: + +1. **Schema Validation** + - YAML structure matches spec + - Required fields present + - No unexpected fields (reject) + +2. **Command Whitelist** + - Only approved commands allowed + - Path validation (no `../../` escapes) + - Environment variable sanitization + - No inline shell operators (`&&`, `|`, `>`) + +3. **Resource Limits** + - Timeout enforced (default: 60 min) + - Memory cap: 16GB (adjustable) + - CPU threads: 4 (adjustable) + - Disk write: 100GB (adjustable) + +4. **Execution Isolation** + - Runs as unprivileged user + - Temp directory cleaned after execution + - Network access: read-only (no outbound writes) + +5. **Audit Trail** + - All executions logged to git + - Commit signed with Ralph's key + - Result stored immutably + +### Threat Mitigations + +| Threat | Mitigation | +|--------|-----------| +| **Malicious task injection** | Branch protection + PR review before merge | +| **Credential leakage** | Pre-commit secret scan + environment scrubbing | +| **Resource exhaustion** | Timeout + memory limits | +| **Code injection** | Command whitelist + no shell evaluation | +| **Result tampering** | Git commit history is immutable | + +--- + +## Configuration + +Ralph reads config from `.squad/config.json`: + +```json +{ + "cross_machine": { + "enabled": true, + "poll_interval_seconds": 300, + "this_machine": "devbox", + "max_concurrent_tasks": 2, + "task_timeout_minutes": 60, + "command_whitelist": [ + "python scripts/voice-clone.py", + "python scripts/data-process.py", + "bash scripts/cleanup.sh" + ], + "result_ttl_days": 30 + } +} +``` + +--- + +## Examples + +### Example 1: GPU Voice Cloning (Laptop → DevBox) + +**1. Laptop creates task:** + +```yaml +# .squad/cross-machine/tasks/2026-03-14T1530Z-laptop-gpu-001.yaml +id: gpu-voice-clone-001 +source_machine: laptop-machine +target_machine: devbox +priority: high +created_at: 2026-03-14T15:30:00Z +task_type: gpu_workload +payload: + command: "python scripts/voice-clone.py --input voice.wav --output cloned.wav" + expected_duration_min: 15 + resources: + gpu: true + memory_gb: 8 +status: pending +``` + +**2. Laptop commits & pushes:** + +```bash +git add .squad/cross-machine/tasks/ +git commit -m "Task: GPU voice cloning [squad:machine-devbox]" +git push origin main +``` + +**3. DevBox Ralph (5 min later):** + +``` +[Ralph Watch Cycle] +- Pulled origin/main +- Detected: gpu-voice-clone-001 (status: pending, target: devbox) +- Validation: āœ… Schema OK, command whitelisted +- Executing: python scripts/voice-clone.py ... +- [15 minutes of processing] +- Completed: exit code 0 +- Writing result... +- Committing & pushing... +``` + +**4. Laptop Ralph (next cycle) sees result:** + +```yaml +# .squad/cross-machine/results/gpu-voice-clone-001.yaml +id: gpu-voice-clone-001 +target_machine: devbox +completed_at: 2026-03-14T15:45:00Z +status: completed +exit_code: 0 +stdout: "Voice cloning completed. Output written to /tmp/cloned.wav" +stderr: "" +duration_seconds: 900 +artifacts: + - path: "/path/to/artifacts/voice-clone-001/output.wav" + type: audio + size_mb: 2.5 +``` + +--- + +### Example 2: Urgent Debug Request (Human → DevBox via Issue) + +**Create issue:** + +```bash +gh issue create \ + --title "DevBox: Debug voice model failure" \ + --body "Error: Model failed to load on last run. Please check /tmp/model.log and report findings." \ + --label "squad:machine-devbox" \ + --label "urgent" +``` + +**DevBox Ralph detects → executes → comments:** + +``` +āœ… Executed on devbox at 2026-03-14 15:47:00 +Command: python scripts/debug-model.py + +Result: +------ +Model file: /tmp/model-v2.bin (OK) +Checksum: a1b2c3d4e5f6 (matches expected) +Memory available: 12 GB (sufficient) + +ERROR FOUND: Config file permission issue + - File: ~/.config/voice/model.yaml + - Permissions: -rw------- (owner-only) + - Expected: -rw-r--r-- (world-readable for service) + +FIX: Run: chmod 644 ~/.config/voice/model.yaml +``` + +--- + +## Error Handling + +### Task Execution Failures + +If a task fails (exit code != 0): + +1. Result written with `status: failed` + exit code +2. stderr captured in result +3. Committed to git for audit +4. Source machine can retry by re-pushing task with `status: pending` + +### Stalled Tasks + +If a task doesn't complete within timeout: + +1. Process killed +2. Result written with `status: timeout` +3. stderr: "Execution exceeded X minutes" +4. Source can investigate or retry + +### Network Failures + +If git push/pull fails: + +- Ralph retries on next cycle +- Tasks queue locally until connectivity restored +- No tasks lost (stored in local repo) + +--- + +## Monitoring & Debugging + +### Check Task Queue + +```bash +ls -la .squad/cross-machine/tasks/ +cat .squad/cross-machine/tasks/*.yaml | grep -E "^(id|status|target_machine):" +``` + +### Check Results + +```bash +ls -la .squad/cross-machine/results/ +cat .squad/cross-machine/results/{task-id}.yaml +``` + +### View Execution History + +```bash +git log --oneline .squad/cross-machine/ | head -20 +``` + +### Monitor Ralph Cycles + +```bash +tail -f .squad/log/ralph-watch.log | grep "cross-machine" +``` + +--- + +## Integration with Ralph Watch + +Ralph automatically includes this pattern in its watch loop: + +``` +Ralph Watch Cycle (every 5-10 min): +1. Fetch GitHub issues with squad:machine-* labels +2. Poll .squad/cross-machine/tasks/ +3. For each matching task: + - Validate + - Execute + - Write result + - Commit & push +4. Update status in issue (if applicable) +5. Sleep until next cycle +``` + +No manual Ralph configuration needed — just create task files or issues with the right labels. + +--- + +## Migration from Manual Handoff + +**Before (today):** +- Laptop → user manually copies file to Teams chat +- user pastes into target terminal +- user copies output back +- user pastes result manually + +**After (with this pattern):** +- Laptop Ralph writes task file → git push +- DevBox Ralph auto-executes → git push result +- Laptop Ralph auto-reads result +- 0 human intervention needed + +--- + +## Future Enhancements + +Potential expansions (Phase 2+): + +1. **Task Priorities:** Execution order based on priority field +2. **Serial Pipelines:** Machine A → B → C task chains +3. **GPU Availability Polling:** Query DevBox before submitting work +4. **Cost Tracking:** Log resource usage per task +5. **Notification Webhooks:** Alert on task completion +6. **Web Dashboard:** Real-time task status visualization + +--- + +## Questions? + +Refer to research report: `research/active/cross-machine-agents/README.md` + +Contact: Seven (Research & Docs) or Ralph (Work Monitor) diff --git a/.squad/templates/skills/cross-squad/SKILL.md b/.squad/templates/skills/cross-squad/SKILL.md new file mode 100644 index 000000000..1d4e3a251 --- /dev/null +++ b/.squad/templates/skills/cross-squad/SKILL.md @@ -0,0 +1,114 @@ +--- +name: "cross-squad" +description: "Coordinating work across multiple Squad instances" +domain: "orchestration" +confidence: "medium" +source: "manual" +tools: + - name: "squad-discover" + description: "List known squads and their capabilities" + when: "When you need to find which squad can handle a task" + - name: "squad-delegate" + description: "Create work in another squad's repository" + when: "When a task belongs to another squad's domain" +--- + +## Context +When an organization runs multiple Squad instances (e.g., platform-squad, frontend-squad, data-squad), those squads need to discover each other, share context, and hand off work across repository boundaries. This skill teaches agents how to coordinate across squads without creating tight coupling. + +Cross-squad orchestration applies when: +- A task requires capabilities owned by another squad +- An architectural decision affects multiple squads +- A feature spans multiple repositories with different squads +- A squad needs to request infrastructure, tooling, or support from another squad + +## Patterns + +### Discovery via Manifest +Each squad publishes a `.squad/manifest.json` declaring its name, capabilities, and contact information. Squads discover each other through: +1. **Well-known paths**: Check `.squad/manifest.json` in known org repos +2. **Upstream config**: Squads already listed in `.squad/upstream.json` are checked for manifests +3. **Explicit registry**: A central `squad-registry.json` can list all squads in an org + +```json +{ + "name": "platform-squad", + "version": "1.0.0", + "description": "Platform infrastructure team", + "capabilities": ["kubernetes", "helm", "monitoring", "ci-cd"], + "contact": { + "repo": "org/platform", + "labels": ["squad:platform"] + }, + "accepts": ["issues", "prs"], + "skills": ["helm-developer", "operator-developer", "pipeline-engineer"] +} +``` + +### Context Sharing +When delegating work, share only what the target squad needs: +- **Capability list**: What this squad can do (from manifest) +- **Relevant decisions**: Only decisions that affect the target squad +- **Handoff context**: A concise description of why this work is being delegated + +Do NOT share: +- Internal team state (casting history, session logs) +- Full decision archives (send only relevant excerpts) +- Authentication credentials or secrets + +### Work Handoff Protocol +1. **Check manifest**: Verify the target squad accepts the work type (issues, PRs) +2. **Create issue**: Use `gh issue create` in the target repo with: + - Title: `[cross-squad] ` + - Label: `squad:cross-squad` (or the squad's configured label) + - Body: Context, acceptance criteria, and link back to originating issue +3. **Track**: Record the cross-squad issue URL in the originating squad's orchestration log +4. **Poll**: Periodically check if the delegated issue is closed/completed + +### Feedback Loop +Track delegated work completion: +- Poll target issue status via `gh issue view` +- Update originating issue with status changes +- Close the feedback loop when delegated work merges + +## Examples + +### Discovering squads +```bash +# List all squads discoverable from upstreams and known repos +squad discover + +# Output: +# platform-squad → org/platform (kubernetes, helm, monitoring) +# frontend-squad → org/frontend (react, nextjs, storybook) +# data-squad → org/data (spark, airflow, dbt) +``` + +### Delegating work +```bash +# Delegate a task to the platform squad +squad delegate platform-squad "Add Prometheus metrics endpoint for the auth service" + +# Creates issue in org/platform with cross-squad label and context +``` + +### Manifest in squad.config.ts +```typescript +export default defineSquad({ + manifest: { + name: 'platform-squad', + capabilities: ['kubernetes', 'helm'], + contact: { repo: 'org/platform', labels: ['squad:platform'] }, + accepts: ['issues', 'prs'], + skills: ['helm-developer', 'operator-developer'], + }, +}); +``` + +## Anti-Patterns +- **Direct file writes across repos** — Never modify another squad's `.squad/` directory. Use issues and PRs as the communication protocol. +- **Tight coupling** — Don't depend on another squad's internal structure. Use the manifest as the public API contract. +- **Unbounded delegation** — Always include acceptance criteria and a timeout. Don't create open-ended requests. +- **Skipping discovery** — Don't hardcode squad locations. Use manifests and the discovery protocol. +- **Sharing secrets** — Never include credentials, tokens, or internal URLs in cross-squad issues. +- **Circular delegation** — Track delegation chains. If squad A delegates to B which delegates back to A, something is wrong. diff --git a/.squad/templates/skills/distributed-mesh/SKILL.md b/.squad/templates/skills/distributed-mesh/SKILL.md new file mode 100644 index 000000000..624db9626 --- /dev/null +++ b/.squad/templates/skills/distributed-mesh/SKILL.md @@ -0,0 +1,287 @@ +--- +name: "distributed-mesh" +description: "How to coordinate with squads on different machines using git as transport" +domain: "distributed-coordination" +confidence: "high" +source: "multi-model-consensus (Opus 4.6, Sonnet 4.5, GPT-5.4)" +--- + +## SCOPE + +**āœ… THIS SKILL PRODUCES (exactly these, nothing more):** + +1. **`mesh.json`** — Generated from user answers about zones and squads (which squads participate, what zone each is in, paths/URLs for each), using `mesh.json.example` in this skill's directory as the schema template +2. **`sync-mesh.sh` and `sync-mesh.ps1`** — Copied from this skill's directory into the project root (these are bundled resources, NOT generated code) +3. **Zone 2 state repo initialization** (if applicable) — If the user specified a Zone 2 shared state repo, run `sync-mesh.sh --init` to scaffold the state repo structure +4. **A decision entry** in `.squad/decisions/inbox/` documenting the mesh configuration for team awareness + +**āŒ THIS SKILL DOES NOT PRODUCE:** + +- **No application code** — No validators, libraries, or modules of any kind +- **No test files** — No test suites, test cases, or test scaffolding +- **No GENERATING sync scripts** — They are bundled with this skill as pre-built resources. COPY them, don't generate them. +- **No daemons or services** — No background processes, servers, or persistent runtimes +- **No modifications to existing squad files** beyond the decision entry (no changes to team.md, routing.md, agent charters, etc.) + +**Your role:** Configure the mesh topology and install the bundled sync scripts. Nothing more. + +## Context + +When squads are on different machines (developer laptops, CI runners, cloud VMs, partner orgs), the local file-reading convention still works — but remote files need to arrive on your disk first. This skill teaches the pattern for distributed squad communication. + +**When this applies:** +- Squads span multiple machines, VMs, or CI runners +- Squads span organizations or companies +- An agent needs context from a squad whose files aren't on the local filesystem + +**When this does NOT apply:** +- All squads are on the same machine (just read the files directly) + +## Patterns + +### The Core Principle + +> "The filesystem is the mesh, and git is how the mesh crosses machine boundaries." + +The agent interface never changes. Agents always read local files. The distributed layer's only job is to make remote files appear locally before the agent reads them. + +### Three Zones of Communication + +**Zone 1 — Local:** Same filesystem. Read files directly. Zero transport. + +**Zone 2 — Remote-Trusted:** Different host, same org, shared git auth. Transport: `git pull` from a shared repo. This collapses Zone 2 into Zone 1 — files materialize on disk, agent reads them normally. + +**Zone 3 — Remote-Opaque:** Different org, no shared auth. Transport: `curl` to fetch published contracts (SUMMARY.md). One-way visibility — you see only what they publish. + +### Agent Lifecycle (Distributed) + +``` +1. SYNC: git pull (Zone 2) + curl (Zone 3) — materialize remote state +2. READ: cat .mesh/**/state.md — all files are local now +3. WORK: do their assigned work (the agent's normal task, NOT mesh-building) +4. WRITE: update own billboard, log, drops +5. PUBLISH: git add + commit + push — share state with remote peers +``` + +Steps 2–4 are identical to local-only. Steps 1 and 5 are the entire distributed extension. **Note:** "WORK" means the agent performs its normal squad duties — it does NOT mean "build mesh infrastructure." + +### The mesh.json Config + +```json +{ + "squads": { + "auth-squad": { "zone": "local", "path": "../auth-squad/.mesh" }, + "ci-squad": { + "zone": "remote-trusted", + "source": "git@github.com:our-org/ci-squad.git", + "ref": "main", + "sync_to": ".mesh/remotes/ci-squad" + }, + "partner-fraud": { + "zone": "remote-opaque", + "source": "https://partner.dev/squad-contracts/fraud/SUMMARY.md", + "sync_to": ".mesh/remotes/partner-fraud", + "auth": "bearer" + } + } +} +``` + +Three zone types, one file. Local squads need only a path. Remote-trusted need a git URL. Remote-opaque need an HTTP URL. + +### Write Partitioning + +Each squad writes only to its own directory (`boards/{self}.md`, `squads/{self}/*`, `drops/{date}-{self}-*.md`). No two squads write to the same file. Git push/pull never conflicts. If push fails ("branch is behind"), the fix is always `git pull --rebase && git push`. + +### Trust Boundaries + +Trust maps to git permissions: +- **Same repo access** = full mesh visibility +- **Read-only access** = can observe, can't write +- **No access** = invisible (correct behavior) + +For selective visibility, use separate repos per audience (internal, partner, public). Git permissions ARE the trust negotiation. + +### Phased Rollout + +- **Phase 0:** Convention only — document zones, agree on mesh.json fields, manually run `git pull`/`git push`. Zero new code. +- **Phase 1:** Sync script (~30 lines bash or PowerShell) when manual sync gets tedious. +- **Phase 2:** Published contracts + curl fetch when a Zone 3 partner appears. +- **Phase 3:** Never. No MCP federation, A2A, service discovery, message queues. + +**Important:** Phases are NOT auto-advanced. These are project-level decisions — you start at Phase 0 (manual sync) and only move forward when the team decides complexity is justified. + +### Mesh State Repo + +The shared mesh state repo is a plain git repository — NOT a Squad project. It holds: +- One directory per participating squad +- Each directory contains at minimum a SUMMARY.md with the squad's current state +- A root README explaining what the repo is and who participates + +No `.squad/` folder, no agents, no automation. Write partitioning means each squad only pushes to its own directory. The repo is a rendezvous point, not an intelligent system. + +If you want a squad that *observes* mesh health, that's a separate Squad project that lists the state repo as a Zone 2 remote in its `mesh.json` — it does NOT live inside the state repo. + +## Examples + +### Developer Laptop + CI Squad (Zone 2) + +Auth-squad agent wakes up. `git pull` brings ci-squad's latest results. Agent reads: "3 test failures in auth module." Adjusts work. Pushes results when done. **Overhead: one `git pull`, one `git push`.** + +### Two Orgs Collaborating (Zone 3) + +Payment-squad fetches partner's published SUMMARY.md via curl. Reads: "Risk scoring v3 API deprecated April 15. New field `device_fingerprint` required." The consuming agent (in payment-squad's team) reads this information and uses it to inform its work — for example, updating payment integration code to include the new field. Partner can't see payment-squad's internals. + +### Same Org, Shared Mesh Repo (Zone 2) + +Three squads on different machines. One shared git repo holds the mesh. Each squad: `git pull` before work, `git push` after. Write partitioning ensures zero merge conflicts. + +## AGENT WORKFLOW (Deterministic Setup) + +When a user invokes this skill to set up a distributed mesh, follow these steps **exactly, in order:** + +### Step 1: ASK the user for mesh topology + +Ask these questions (adapt phrasing naturally, but get these answers): + +1. **Which squads are participating?** (List of squad names) +2. **For each squad, which zone is it in?** + - `local` — same filesystem (just need a path) + - `remote-trusted` — different machine, same org, shared git access (need git URL + ref) + - `remote-opaque` — different org, no shared auth (need HTTPS URL to published contract) +3. **For each squad, what's the connection info?** + - Local: relative or absolute path to their `.mesh/` directory + - Remote-trusted: git URL (SSH or HTTPS), ref (branch/tag), and where to sync it to locally + - Remote-opaque: HTTPS URL to their SUMMARY.md, where to sync it, and auth type (none/bearer) +4. **Where should the shared state live?** (For Zone 2 squads: git repo URL for the mesh state, or confirm each squad syncs independently) + +### Step 2: GENERATE `mesh.json` + +Using the answers from Step 1, create a `mesh.json` file at the project root. Use `mesh.json.example` from THIS skill's directory (`.squad/skills/distributed-mesh/mesh.json.example`) as the schema template. + +Structure: + +```json +{ + "squads": { + "": { "zone": "local", "path": "" }, + "": { + "zone": "remote-trusted", + "source": "", + "ref": "", + "sync_to": ".mesh/remotes/" + }, + "": { + "zone": "remote-opaque", + "source": "", + "sync_to": ".mesh/remotes/", + "auth": "" + } + } +} +``` + +Write this file to the project root. Do NOT write any other code. + +### Step 3: COPY sync scripts + +Copy the bundled sync scripts from THIS skill's directory into the project root: + +- **Source:** `.squad/skills/distributed-mesh/sync-mesh.sh` +- **Destination:** `sync-mesh.sh` (project root) + +- **Source:** `.squad/skills/distributed-mesh/sync-mesh.ps1` +- **Destination:** `sync-mesh.ps1` (project root) + +These are bundled resources. Do NOT generate them — COPY them directly. + +### Step 4: RUN `--init` (if Zone 2 state repo exists) + +If the user specified a Zone 2 shared state repo in Step 1, run the initialization: + +**On Unix/Linux/macOS:** +```bash +bash sync-mesh.sh --init +``` + +**On Windows:** +```powershell +.\sync-mesh.ps1 -Init +``` + +This scaffolds the state repo structure (squad directories, placeholder SUMMARY.md files, root README). + +**Skip this step if:** +- No Zone 2 squads are configured (local/opaque only) +- The state repo already exists and is initialized + +### Step 5: WRITE a decision entry + +Create a decision file at `.squad/decisions/inbox/-mesh-setup.md` with this content: + +```markdown +### : Mesh configuration + +**By:** (via distributed-mesh skill) + +**What:** Configured distributed mesh with squads across zones + +**Squads:** +- `` — Zone — +- `` — Zone — +- ... + +**State repo:** + +**Why:** +``` + +Write this file. The Scribe will merge it into the main decisions file later. + +### Step 6: STOP + +**You are done.** Do not: +- Generate sync scripts (they're bundled with this skill — COPY them) +- Write validator code +- Write test files +- Create any other modules, libraries, or application code +- Modify existing squad files (team.md, routing.md, charters) +- Auto-advance to Phase 2 or Phase 3 + +Output a simple completion message: + +``` +āœ… Mesh configured. Created: +- mesh.json ( squads) +- sync-mesh.sh and sync-mesh.ps1 (copied from skill bundle) +- Decision entry: .squad/decisions/inbox/ + +Run `bash sync-mesh.sh` (or `.\sync-mesh.ps1` on Windows) before agents start to materialize remote state. +``` + +--- + +## Anti-Patterns + +**āŒ Code generation anti-patterns:** +- Writing `mesh-config-validator.js` or any validator module +- Writing test files for mesh configuration +- Generating sync scripts instead of copying the bundled ones from this skill's directory +- Creating library modules or utilities +- Building any code that "runs the mesh" — the mesh is read by agents, not executed + +**āŒ Architectural anti-patterns:** +- Building a federation protocol — Git push/pull IS federation +- Running a sync daemon or server — Agents are not persistent. Sync at startup, publish at shutdown +- Real-time notifications — Agents don't need real-time. They need "recent enough." `git pull` is recent enough +- Schema validation for markdown — The LLM reads markdown. If the format changes, it adapts +- Service discovery protocol — mesh.json is a file with 10 entries. Not a "discovery problem" +- Auth framework — Git SSH keys and HTTPS tokens. Not a framework. Already configured +- Message queues / event buses — Agents wake, read, work, write, sleep. Nobody's home to receive events +- Any component requiring a running process — That's the line. Don't cross it + +**āŒ Scope creep anti-patterns:** +- Auto-advancing phases without user decision +- Modifying agent charters or routing rules +- Setting up CI/CD pipelines for mesh sync +- Creating dashboards or monitoring tools diff --git a/.squad/templates/skills/distributed-mesh/mesh.json.example b/.squad/templates/skills/distributed-mesh/mesh.json.example new file mode 100644 index 000000000..7f5730a88 --- /dev/null +++ b/.squad/templates/skills/distributed-mesh/mesh.json.example @@ -0,0 +1,30 @@ +{ + "squads": { + "auth-squad": { + "zone": "local", + "path": "../auth-squad/.mesh" + }, + "api-squad": { + "zone": "local", + "path": "../api-squad/.mesh" + }, + "ci-squad": { + "zone": "remote-trusted", + "source": "git@github.com:our-org/ci-squad.git", + "ref": "main", + "sync_to": ".mesh/remotes/ci-squad" + }, + "data-squad": { + "zone": "remote-trusted", + "source": "git@github.com:our-org/data-pipeline.git", + "ref": "main", + "sync_to": ".mesh/remotes/data-squad" + }, + "partner-fraud": { + "zone": "remote-opaque", + "source": "https://partner.example.com/squad-contracts/fraud/SUMMARY.md", + "sync_to": ".mesh/remotes/partner-fraud", + "auth": "bearer" + } + } +} diff --git a/.squad/templates/skills/distributed-mesh/sync-mesh.ps1 b/.squad/templates/skills/distributed-mesh/sync-mesh.ps1 new file mode 100644 index 000000000..5f409ef37 --- /dev/null +++ b/.squad/templates/skills/distributed-mesh/sync-mesh.ps1 @@ -0,0 +1,111 @@ +# sync-mesh.ps1 — Materialize remote squad state locally +# +# Reads mesh.json, fetches remote squads into local directories. +# Run before agent reads. No daemon. No service. ~40 lines. +# +# Usage: .\sync-mesh.ps1 [path-to-mesh.json] +# .\sync-mesh.ps1 -Init [path-to-mesh.json] +# Requires: git +param( + [switch]$Init, + [string]$MeshJson = "mesh.json" +) +$ErrorActionPreference = "Stop" + +# Handle -Init mode +if ($Init) { + if (-not (Test-Path $MeshJson)) { + Write-Host "āŒ $MeshJson not found" + exit 1 + } + + Write-Host "šŸš€ Initializing mesh state repository..." + $config = Get-Content $MeshJson -Raw | ConvertFrom-Json + $squads = $config.squads.PSObject.Properties.Name + + # Create squad directories with placeholder SUMMARY.md + foreach ($squad in $squads) { + if (-not (Test-Path $squad)) { + New-Item -ItemType Directory -Path $squad | Out-Null + Write-Host " āœ“ Created $squad/" + } else { + Write-Host " • $squad/ exists (skipped)" + } + + $summaryPath = "$squad/SUMMARY.md" + if (-not (Test-Path $summaryPath)) { + "# $squad`n`n_No state published yet._" | Set-Content $summaryPath + Write-Host " āœ“ Created $summaryPath" + } else { + Write-Host " • $summaryPath exists (skipped)" + } + } + + # Generate root README.md + if (-not (Test-Path "README.md")) { + $readme = @" +# Squad Mesh State Repository + +This repository tracks published state from participating squads. + +## Participating Squads + +"@ + foreach ($squad in $squads) { + $zone = $config.squads.$squad.zone + $readme += "- **$squad** (Zone: $zone)`n" + } + $readme += @" + +Each squad directory contains a ``SUMMARY.md`` with their latest published state. +State is synchronized using ``sync-mesh.sh`` or ``sync-mesh.ps1``. +"@ + $readme | Set-Content "README.md" + Write-Host " āœ“ Created README.md" + } else { + Write-Host " • README.md exists (skipped)" + } + + Write-Host "" + Write-Host "āœ… Mesh state repository initialized" + exit 0 +} + +$config = Get-Content $MeshJson -Raw | ConvertFrom-Json + +# Zone 2: Remote-trusted — git clone/pull +foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-trusted" }) { + $squad = $entry.Name + $source = $entry.Value.source + $ref = if ($entry.Value.ref) { $entry.Value.ref } else { "main" } + $target = $entry.Value.sync_to + + if (Test-Path "$target/.git") { + git -C $target pull --rebase --quiet 2>$null + if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: pull failed (using stale)" } + } else { + New-Item -ItemType Directory -Force -Path (Split-Path $target -Parent) | Out-Null + git clone --quiet --depth 1 --branch $ref $source $target 2>$null + if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: clone failed (unavailable)" } + } +} + +# Zone 3: Remote-opaque — fetch published contracts +foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-opaque" }) { + $squad = $entry.Name + $source = $entry.Value.source + $target = $entry.Value.sync_to + $auth = $entry.Value.auth + + New-Item -ItemType Directory -Force -Path $target | Out-Null + $params = @{ Uri = $source; OutFile = "$target/SUMMARY.md"; UseBasicParsing = $true } + if ($auth -eq "bearer") { + $tokenVar = ($squad.ToUpper() -replace '-', '_') + "_TOKEN" + $token = [Environment]::GetEnvironmentVariable($tokenVar) + if ($token) { $params.Headers = @{ Authorization = "Bearer $token" } } + } + try { Invoke-WebRequest @params -ErrorAction Stop } + catch { "# ${squad} — unavailable ($(Get-Date))" | Set-Content "$target/SUMMARY.md" } +} + +Write-Host "āœ“ Mesh sync complete" diff --git a/.squad/templates/skills/distributed-mesh/sync-mesh.sh b/.squad/templates/skills/distributed-mesh/sync-mesh.sh new file mode 100644 index 000000000..802fd2d8d --- /dev/null +++ b/.squad/templates/skills/distributed-mesh/sync-mesh.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# sync-mesh.sh — Materialize remote squad state locally +# +# Reads mesh.json, fetches remote squads into local directories. +# Run before agent reads. No daemon. No service. ~40 lines. +# +# Usage: ./sync-mesh.sh [path-to-mesh.json] +# ./sync-mesh.sh --init [path-to-mesh.json] +# Requires: jq (https://github.com/jqlang/jq), git, curl + +set -euo pipefail + +# Handle --init mode +if [ "${1:-}" = "--init" ]; then + MESH_JSON="${2:-mesh.json}" + + if [ ! -f "$MESH_JSON" ]; then + echo "āŒ $MESH_JSON not found" + exit 1 + fi + + echo "šŸš€ Initializing mesh state repository..." + squads=$(jq -r '.squads | keys[]' "$MESH_JSON") + + # Create squad directories with placeholder SUMMARY.md + for squad in $squads; do + if [ ! -d "$squad" ]; then + mkdir -p "$squad" + echo " āœ“ Created $squad/" + else + echo " • $squad/ exists (skipped)" + fi + + if [ ! -f "$squad/SUMMARY.md" ]; then + echo -e "# $squad\n\n_No state published yet._" > "$squad/SUMMARY.md" + echo " āœ“ Created $squad/SUMMARY.md" + else + echo " • $squad/SUMMARY.md exists (skipped)" + fi + done + + # Generate root README.md + if [ ! -f "README.md" ]; then + { + echo "# Squad Mesh State Repository" + echo "" + echo "This repository tracks published state from participating squads." + echo "" + echo "## Participating Squads" + echo "" + for squad in $squads; do + zone=$(jq -r ".squads.\"$squad\".zone" "$MESH_JSON") + echo "- **$squad** (Zone: $zone)" + done + echo "" + echo "Each squad directory contains a \`SUMMARY.md\` with their latest published state." + echo "State is synchronized using \`sync-mesh.sh\` or \`sync-mesh.ps1\`." + } > README.md + echo " āœ“ Created README.md" + else + echo " • README.md exists (skipped)" + fi + + echo "" + echo "āœ… Mesh state repository initialized" + exit 0 +fi + +MESH_JSON="${1:-mesh.json}" + +# Zone 2: Remote-trusted — git clone/pull +for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-trusted") | .key' "$MESH_JSON"); do + source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON") + ref=$(jq -r ".squads.\"$squad\".ref // \"main\"" "$MESH_JSON") + target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON") + + if [ -d "$target/.git" ]; then + git -C "$target" pull --rebase --quiet 2>/dev/null \ + || echo "⚠ $squad: pull failed (using stale)" + else + mkdir -p "$(dirname "$target")" + git clone --quiet --depth 1 --branch "$ref" "$source" "$target" 2>/dev/null \ + || echo "⚠ $squad: clone failed (unavailable)" + fi +done + +# Zone 3: Remote-opaque — fetch published contracts +for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-opaque") | .key' "$MESH_JSON"); do + source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON") + target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON") + auth=$(jq -r ".squads.\"$squad\".auth // \"\"" "$MESH_JSON") + + mkdir -p "$target" + auth_flag="" + if [ "$auth" = "bearer" ]; then + token_var="$(echo "${squad}" | tr '[:lower:]-' '[:upper:]_')_TOKEN" + [ -n "${!token_var:-}" ] && auth_flag="--header \"Authorization: Bearer ${!token_var}\"" + fi + + eval curl --silent --fail $auth_flag "$source" -o "$target/SUMMARY.md" 2>/dev/null \ + || echo "# ${squad} — unavailable ($(date))" > "$target/SUMMARY.md" +done + +echo "āœ“ Mesh sync complete" diff --git a/.squad/templates/skills/docs-standards/SKILL.md b/.squad/templates/skills/docs-standards/SKILL.md new file mode 100644 index 000000000..c30c54e4b --- /dev/null +++ b/.squad/templates/skills/docs-standards/SKILL.md @@ -0,0 +1,71 @@ +--- +name: "docs-standards" +description: "Microsoft Style Guide + Squad-specific documentation patterns" +domain: "documentation" +confidence: "high" +source: "earned (PAO charter, multiple doc PR reviews)" +--- + +## Context + +Squad documentation follows the Microsoft Style Guide with Squad-specific conventions. Consistency across docs builds trust and improves discoverability. + +## Patterns + +### Microsoft Style Guide Rules +- **Sentence-case headings:** "Getting started" not "Getting Started" +- **Active voice:** "Run the command" not "The command should be run" +- **Second person:** "You can configure..." not "Users can configure..." +- **Present tense:** "The system routes..." not "The system will route..." +- **No ampersands in prose:** "and" not "&" (except in code, brand names, or UI elements) + +### Squad Formatting Patterns +- **Scannability first:** Paragraphs for narrative (3-4 sentences max), bullets for scannable lists, tables for structured data +- **"Try this" prompts at top:** Start feature/scenario pages with practical prompts users can copy +- **Experimental warnings:** Features in preview get callout at top +- **Cross-references at bottom:** Related pages linked after main content + +### Structure +- **Title (H1)** → **Warning/callout** → **Try this code** → **Overview** → **HR** → **Content (H2 sections)** + +### Test Sync Rule +- **Always update test assertions:** When adding docs pages to `features/`, `scenarios/`, `guides/`, update corresponding `EXPECTED_*` arrays in `test/docs-build.test.ts` in the same commit + +## Examples + +āœ“ **Correct:** +```markdown +# Getting started with Squad + +> āš ļø **Experimental:** This feature is in preview. + +Try this: +\`\`\`bash +squad init +\`\`\` + +Squad helps you build AI teams... + +--- + +## Install Squad + +Run the following command... +``` + +āœ— **Incorrect:** +```markdown +# Getting Started With Squad // Title case + +Squad is a tool which will help users... // Third person, future tense + +You can install Squad with npm & configure it... // Ampersand in prose +``` + +## Anti-Patterns + +- Title-casing headings because "it looks nicer" +- Writing in passive voice or third person +- Long paragraphs of dense text (breaks scannability) +- Adding doc pages without updating test assertions +- Using ampersands outside code blocks diff --git a/.squad/templates/skills/economy-mode/SKILL.md b/.squad/templates/skills/economy-mode/SKILL.md new file mode 100644 index 000000000..696e778c4 --- /dev/null +++ b/.squad/templates/skills/economy-mode/SKILL.md @@ -0,0 +1,114 @@ +--- +name: "economy-mode" +description: "Shifts Layer 3 model selection to cost-optimized alternatives when economy mode is active." +domain: "model-selection" +confidence: "low" +source: "manual" +--- + +## SCOPE + +āœ… THIS SKILL PRODUCES: +- A modified Layer 3 model selection table applied when economy mode is active +- `economyMode: true` written to `.squad/config.json` when activated persistently +- Spawn acknowledgments with `šŸ’°` indicator when economy mode is active + +āŒ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Cost reports or billing artifacts +- Changes to Layer 0, Layer 1, or Layer 2 resolution (user intent always wins) + +## Context + +Economy mode shifts Layer 3 (Task-Aware Auto-Selection) to lower-cost alternatives. It does NOT override persistent config (`defaultModel`, `agentModelOverrides`) or per-agent charter preferences — those represent explicit user intent and always take priority. + +Use this skill when the user wants to reduce costs across an entire session or permanently, without manually specifying models for each agent. + +## Activation Methods + +| Method | How | +|--------|-----| +| Session phrase | "use economy mode", "save costs", "go cheap", "reduce costs" | +| Persistent config | `"economyMode": true` in `.squad/config.json` | +| CLI flag | `squad --economy` | + +**Deactivation:** "turn off economy mode", "disable economy mode", or remove `economyMode` from `config.json`. + +## Economy Model Selection Table + +When economy mode is **active**, Layer 3 auto-selection uses this table instead of the normal defaults: + +| Task Output | Normal Mode | Economy Mode | +|-------------|-------------|--------------| +| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | +| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | + +**Prefer `gpt-4.1` over `gpt-5-mini`** when the task involves structured output or agentic tool use. Prefer `gpt-5-mini` for pure text generation tasks where latency matters. + +## AGENT WORKFLOW + +### On Session Start + +1. READ `.squad/config.json` +2. CHECK for `economyMode: true` — if present, activate economy mode for the session +3. STORE economy mode state in session context + +### On User Phrase Trigger + +**Session-only (no config change):** "use economy mode", "save costs", "go cheap" + +1. SET economy mode active for this session +2. ACKNOWLEDGE: `āœ… Economy mode active — using cost-optimized models this session. (Layer 0 and Layer 2 preferences still apply)` + +**Persistent:** "always use economy mode", "save economy mode" + +1. WRITE `economyMode: true` to `.squad/config.json` (merge, don't overwrite other fields) +2. ACKNOWLEDGE: `āœ… Economy mode saved — cost-optimized models will be used until disabled.` + +### On Every Agent Spawn (Economy Mode Active) + +1. CHECK Layer 0a/0b first (agentModelOverrides, defaultModel) — if set, use that. Economy mode does NOT override Layer 0. +2. CHECK Layer 1 (session directive for a specific model) — if set, use that. Economy mode does NOT override explicit session directives. +3. CHECK Layer 2 (charter preference) — if set, use that. Economy mode does NOT override charter preferences. +4. APPLY economy table at Layer 3 instead of normal table. +5. INCLUDE `šŸ’°` in spawn acknowledgment: `šŸ”§ {Name} ({model} Ā· šŸ’° economy) — {task}` + +### On Deactivation + +**Trigger phrases:** "turn off economy mode", "disable economy mode", "use normal models" + +1. REMOVE `economyMode` from `.squad/config.json` (if it was persisted) +2. CLEAR session economy mode state +3. ACKNOWLEDGE: `āœ… Economy mode disabled — returning to standard model selection.` + +### STOP + +After updating economy mode state and including the `šŸ’°` indicator in spawn acknowledgments, this skill is done. Do NOT: +- Change Layer 0, Layer 1, or Layer 2 model choices +- Override charter-specified models +- Generate cost reports or comparisons +- Fall back to premium models via economy mode (economy mode never bumps UP) + +## Config Schema + +`.squad/config.json` economy-related fields: + +```json +{ + "version": 1, + "economyMode": true +} +``` + +- `economyMode` — when `true`, Layer 3 uses the economy table. Optional; absent = economy mode off. +- Combines with `defaultModel` and `agentModelOverrides` — Layer 0 always wins. + +## Anti-Patterns + +- **Don't override Layer 0 in economy mode.** If the user set `defaultModel: "claude-opus-4.6"`, they want quality. Economy mode only affects Layer 3 auto-selection. +- **Don't silently apply economy mode.** Always acknowledge when activated or deactivated. +- **Don't treat economy mode as permanent by default.** Session phrases activate session-only; only "always" or `config.json` persist it. +- **Don't bump premium tasks down too far.** Architecture and security reviews shift from opus to sonnet in economy mode — they do NOT go to fast/cheap models. diff --git a/.squad/templates/skills/error-recovery/SKILL.md b/.squad/templates/skills/error-recovery/SKILL.md new file mode 100644 index 000000000..ebf38825c --- /dev/null +++ b/.squad/templates/skills/error-recovery/SKILL.md @@ -0,0 +1,99 @@ +--- +name: "error-recovery" +description: "Standard recovery patterns for all squad agents. When something fails, adapt — don't just report the failure." +domain: "reliability, agent-coordination" +confidence: "high" +license: MIT +--- + +# Error Recovery Patterns + +Standard recovery patterns for all squad agents. When something fails, **adapt** — don't just report the failure. + +--- + +## 1. Retry with Backoff + +**When:** Transient failures — API timeouts, rate limits, network errors, temporary service unavailability. + +**Pattern:** +1. Wait briefly, then retry (start at 2s, double each attempt) +2. Maximum 3 retries before escalating +3. Log each attempt with the error received + +**Example:** API call returns 429 Too Many Requests → wait 2s → retry → wait 4s → retry → wait 8s → retry → escalate if still failing. + +--- + +## 2. Fallback Alternatives + +**When:** Primary tool or approach fails and an alternative exists. + +**Pattern:** +1. Attempt primary approach +2. On failure, identify alternative tool/method +3. Try the alternative with the same intent +4. Document which alternative was used and why + +**Example:** Primary CLI tool fails → fall back to direct API call for the same operation. + +--- + +## 3. Diagnose-and-Fix + +**When:** Build failures, test failures, linting errors — structured errors with actionable output. + +**Pattern:** +1. Read the full error output carefully +2. Identify the root cause from error messages +3. Attempt a targeted fix +4. Re-run to verify the fix +5. Maximum 3 fix-retry cycles before escalating + +**Example:** Build fails with a type error → check for missing import → add it → rebuild. + +--- + +## 4. Escalate with Context + +**When:** Recovery attempts have been exhausted, or the failure requires human judgment. + +**Pattern:** +1. Summarize what was attempted and what failed +2. Include the exact error messages +3. State what you believe the root cause is +4. Suggest next steps or who might be able to help +5. Hand off to the coordinator or the appropriate specialist + +**Example:** After 3 failed build attempts → "Build fails on line 42 with null reference. Tried X, Y, Z. Likely a design issue in the Foo module. Recommend the code owner review." + +--- + +## 5. Graceful Degradation + +**When:** A non-critical step fails but the overall task can still deliver value. + +**Pattern:** +1. Determine if the failed step is critical to the task outcome +2. If non-critical, log the failure and continue +3. Deliver partial results with a clear note of what was skipped +4. Offer to retry the skipped step separately + +**Example:** Generating a report with 5 sections — section 3 data source is unavailable → produce the report with 4 sections, note that section 3 was skipped and why. + +--- + +## Applying These Patterns + +Each agent should reference these patterns in their charter's `## Error Recovery` section, tailored to their domain. The charter should list the agent's most common failure modes and map each to the appropriate pattern above. + +**Selection guide:** + +| Failure Type | Primary Pattern | Fallback Pattern | +|---|---|---| +| Network/API transient | Retry with Backoff | Escalate with Context | +| Tool/dependency missing | Fallback Alternatives | Escalate with Context | +| Build/test error | Diagnose-and-Fix | Escalate with Context | +| Auth/permissions | Retry with Backoff | Escalate with Context | +| Non-critical data missing | Graceful Degradation | — | +| Unknown/novel error | Escalate with Context | — | diff --git a/.squad/templates/skills/external-comms/SKILL.md b/.squad/templates/skills/external-comms/SKILL.md new file mode 100644 index 000000000..9ac372dca --- /dev/null +++ b/.squad/templates/skills/external-comms/SKILL.md @@ -0,0 +1,329 @@ +--- +name: "external-comms" +description: "PAO workflow for scanning, drafting, and presenting community responses with human review gate" +domain: "community, communication, workflow" +confidence: "low" +source: "manual (RFC #426 — PAO External Communications)" +tools: + - name: "github-mcp-server-list_issues" + description: "List open issues for scan candidates and lightweight triage" + when: "Use for recent open issue scans before thread-level review" + - name: "github-mcp-server-issue_read" + description: "Read the full issue, comments, and labels before drafting" + when: "Use after selecting a candidate so PAO has complete thread context" + - name: "github-mcp-server-search_issues" + description: "Search for candidate issues or prior squad responses" + when: "Use when filtering by keywords, labels, or duplicate response checks" + - name: "gh CLI" + description: "Fallback for GitHub issue comments and discussions workflows" + when: "Use gh issue list/comment and gh api or gh api graphql when MCP coverage is incomplete" +--- + +## Context + +Phase 1 is **draft-only mode**. + +- PAO scans issues and discussions, drafts responses with the humanizer skill, and presents a review table for human approval. +- **Human review gate is mandatory** — PAO never posts autonomously. +- Every action is logged to `.squad/comms/audit/`. +- This workflow is triggered manually only ("PAO, check community") — no automated or Ralph-triggered activation in Phase 1. + +## Patterns + +### 1. Scan + +Find unanswered community items with GitHub MCP tools first, or `gh issue list` / `gh api` as fallback for issues and discussions. + +- Include **open** issues and discussions only. +- Filter for items with **no squad team response**. +- Limit to items created in the last 7 days. +- Exclude items labeled `squad:internal` or `wontfix`. +- Include discussions **and** issues in the same sweep. +- Phase 1 scope is **issues and discussions only** — do not draft PR replies. + +### Discussion Handling (Phase 1) + +Discussions use the GitHub Discussions API, which differs from issues: + +- **Scan:** `gh api /repos/{owner}/{repo}/discussions --jq '.[] | select(.answer_chosen_at == null)'` to find unanswered discussions +- **Categories:** Filter by Q&A and General categories only (skip Announcements, Show and Tell) +- **Answers vs comments:** In Q&A discussions, PAO drafts an "answer" (not a comment). The human marks it as accepted answer after posting. +- **Phase 1 scope:** Issues and Discussions ONLY. No PR comments. + +### 2. Classify + +Determine the response type before drafting. + +- Welcome (new contributor) +- Troubleshooting (bug/help) +- Feature guidance (feature request/how-to) +- Redirect (wrong repo/scope) +- Acknowledgment (confirmed, no fix) +- Closing (resolved) +- Technical uncertainty (unknown cause) +- Empathetic disagreement (pushback on a decision or design) +- Information request (need more reproduction details or context) + +### Template Selection Guide + +| Signal in Issue/Discussion | → Response Type | Template | +|---------------------------|-----------------|----------| +| New contributor (0 prior issues) | Welcome | T1 | +| Error message, stack trace, "doesn't work" | Troubleshooting | T2 | +| "How do I...?", "Can Squad...?", "Is there a way to...?" | Feature Guidance | T3 | +| Wrong repo, out of scope for Squad | Redirect | T4 | +| Confirmed bug, no fix available yet | Acknowledgment | T5 | +| Fix shipped, PR merged that resolves issue | Closing | T6 | +| Unclear cause, needs investigation | Technical Uncertainty | T7 | +| Author disagrees with a decision or design | Empathetic Disagreement | T8 | +| Need more reproduction info or context | Information Request | T9 | + +Use exactly one template as the base draft. Replace placeholders with issue-specific details, then apply the humanizer patterns. If the thread spans multiple signals, choose the highest-risk template and capture the nuance in the thread summary. + +### Confidence Classification + +| Confidence | Criteria | Example | +|-----------|----------|---------| +| 🟢 High | Answer exists in Squad docs or FAQ, similar question answered before, no technical ambiguity | "How do I install Squad?" | +| 🟔 Medium | Technical answer is sound but involves judgment calls, OR docs exist but don't perfectly match the question, OR tone is tricky | "Can Squad work with Azure DevOps?" (yes, but setup is nuanced) | +| šŸ”“ Needs Review | Technical uncertainty, policy/roadmap question, potential reputational risk, author is frustrated/angry, question about unreleased features | "When will Squad support Claude?" | + +**Auto-escalation rules:** +- Any mention of competitors → šŸ”“ +- Any mention of pricing/licensing → šŸ”“ +- Author has >3 follow-up comments without resolution → šŸ”“ +- Question references a closed-wontfix issue → šŸ”“ + +### 3. Draft + +Use the humanizer skill for every draft. + +- Complete **Thread-Read Verification** before writing. +- Read the **full thread**, including all comments, before writing. +- Select the matching template from the **Template Selection Guide** and record the template ID in the review notes. +- Treat templates as reusable drafting assets: keep the structure, replace placeholders, and only improvise when the thread truly requires it. +- Validate the draft against the humanizer anti-patterns. +- Flag long threads (`>10` comments) with `āš ļø`. + +### Thread-Read Verification + +Before drafting, PAO MUST verify complete thread coverage: + +1. **Count verification:** Compare API comment count with actually-read comments. If mismatch, abort draft. +2. **Deleted comment check:** Use `gh api` timeline to detect deleted comments. If found, flag as āš ļø in review table. +3. **Thread summary:** Include in every draft: "Thread: {N} comments, last activity {date}, {summary of key points}" +4. **Long thread flag:** If >10 comments, add āš ļø to review table and include condensed thread summary +5. **Evidence line in review table:** Each draft row includes "Read: {N}/{total} comments" column + +### 4. Present + +Show drafts for review in this exact format: + +```text +šŸ“ PAO — Community Response Drafts +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +| # | Item | Author | Type | Confidence | Read | Preview | +|---|------|--------|------|------------|------|---------| +| 1 | Issue #N | @user | Type | 🟢/🟔/šŸ”“ | N/N | "First words..." | + +Confidence: 🟢 High | 🟔 Medium | šŸ”“ Needs review + +Full drafts below ā–¼ +``` + +Each full draft must begin with the thread summary line: +`Thread: {N} comments, last activity {date}, {summary of key points}` + +### 5. Human Action + +Wait for explicit human direction before anything is posted. + +- `pao approve 1 3` — approve drafts 1 and 3 +- `pao edit 2` — edit draft 2 +- `pao skip` — skip all +- `banana` — freeze all pending (safe word) + +### Rollback — Bad Post Recovery + +If a posted response turns out to be wrong, inappropriate, or needs correction: + +1. **Delete the comment:** + - Issues: `gh api -X DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}` + - Discussions: `gh api graphql -f query='mutation { deleteDiscussionComment(input: {id: "{node_id}"}) { comment { id } } }'` +2. **Log the deletion:** Write audit entry with action `delete`, include reason and original content +3. **Draft replacement** (if needed): PAO drafts a corrected response, goes through normal review cycle +4. **Postmortem:** If the error reveals a pattern gap, update humanizer anti-patterns or add a new test case + +**Safe word — `banana`:** +- Immediately freezes all pending drafts in the review queue +- No new scans or drafts until `pao resume` is issued +- Audit entry logged with halter identity and reason + +### 6. Post + +After approval: + +- Human posts via `gh issue comment` for issues or `gh api` for discussion answers/comments. +- PAO helps by preparing the CLI command. +- Write the audit entry after the posting action. + +### 7. Audit + +Log every action. + +- Location: `.squad/comms/audit/{timestamp}.md` +- Required fields vary by action — see `.squad/comms/templates/audit-entry.md` Conditional Fields table +- Universal required fields: `timestamp`, `action` +- All other fields are conditional on the action type + +## Examples + +These are reusable templates. Keep the structure, replace placeholders, and adjust only where the thread requires it. + +### Example scan command + +```bash +gh issue list --state open --json number,title,author,labels,comments --limit 20 +``` + +### Example review table + +```text +šŸ“ PAO — Community Response Drafts +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +| # | Item | Author | Type | Confidence | Read | Preview | +|---|------|--------|------|------------|------|---------| +| 1 | Issue #426 | @newdev | Welcome | 🟢 | 1/1 | "Hey @newdev! Welcome to Squad..." | +| 2 | Discussion #18 | @builder | Feature guidance | 🟔 | 4/4 | "Great question! Today the CLI..." | +| 3 | Issue #431 āš ļø | @debugger | Technical uncertainty | šŸ”“ | 12/12 | "Interesting find, @debugger..." | + +Confidence: 🟢 High | 🟔 Medium | šŸ”“ Needs review + +Full drafts below ā–¼ +``` + +### Example audit entry (post action) + +```markdown +--- +timestamp: "2026-03-16T21:30:00Z" +action: "post" +item_number: 426 +draft_id: 1 +reviewer: "@bradygaster" +--- + +## Context (draft, approve, edit, skip, post, delete actions) +- Thread depth: 3 +- Response type: welcome +- Confidence: 🟢 +- Long thread flag: false + +## Draft Content (draft, edit, post actions) +Thread: 3 comments, last activity 2026-03-16, reporter hit a preview-build regression after install. + +Hey @newdev! Welcome to Squad šŸ‘‹ Thanks for opening this. +We reproduced the issue in preview builds and we're checking the regression point now. +Let us know if you can share the command you ran right before the failure. + +## Post Result (post, delete actions) +https://github.com/bradygaster/squad/issues/426#issuecomment-123456 +``` + +### T1 — Welcome + +```text +Hey {author}! Welcome to Squad šŸ‘‹ Thanks for opening this. +{specific acknowledgment or first answer} +Let us know if you have questions — happy to help! +``` + +### T2 — Troubleshooting + +```text +Thanks for the detailed report, {author}! +Here's what we think is happening: {explanation} +{steps or workaround} +Let us know if that helps, or if you're seeing something different. +``` + +### T3 — Feature Guidance + +```text +Great question! {context on current state} +{guidance or workaround} +We've noted this as a potential improvement — {tracking info if applicable}. +``` + +### T4 — Redirect + +```text +Thanks for reaching out! This one is actually better suited for {correct location}. +{brief explanation of why} +Feel free to open it there — they'll be able to help! +``` + +### T5 — Acknowledgment + +```text +Good catch, {author}. We've confirmed this is a real issue. +{what we know so far} +We'll update this thread when we have a fix. Thanks for flagging it! +``` + +### T6 — Closing + +```text +This should be resolved in {version/PR}! šŸŽ‰ +{brief summary of what changed} +Thanks for reporting this, {author} — it made Squad better. +``` + +### T7 — Technical Uncertainty + +```text +Interesting find, {author}. We're not 100% sure what's causing this yet. +Here's what we've ruled out: {list} +We'd love more context if you have it — {specific ask}. +We'll dig deeper and update this thread. +``` + +### T8 — Empathetic Disagreement + +```text +We hear you, {author}. That's a fair concern. + +The current design choice was driven by {reason}. We know it's not ideal for every use case. + +{what alternatives exist or what trade-off was made} + +If you have ideas for how to make this work better for your scenario, we'd love to hear them — open a discussion or drop your thoughts here! +``` + +### T9 — Information Request + +```text +Thanks for reporting this, {author}! + +To help us dig into this, could you share: +- {specific ask 1} +- {specific ask 2} +- {specific ask 3, if applicable} + +That context will help us narrow down what's happening. Appreciate it! +``` + +## Anti-Patterns + +- āŒ Posting without human review (NEVER — this is the cardinal rule) +- āŒ Drafting without reading full thread (context is everything) +- āŒ Ignoring confidence flags (šŸ”“ items need Flight/human review) +- āŒ Scanning closed issues (only open items) +- āŒ Responding to issues labeled `squad:internal` or `wontfix` +- āŒ Skipping audit logging (every action must be recorded) +- āŒ Drafting for issues where a squad member already responded (avoid duplicates) +- āŒ Drafting pull request responses in Phase 1 (issues/discussions only) +- āŒ Treating templates like loose examples instead of reusable drafting assets +- āŒ Asking for more info without specific requests diff --git a/.squad/templates/skills/gh-auth-isolation/SKILL.md b/.squad/templates/skills/gh-auth-isolation/SKILL.md new file mode 100644 index 000000000..e4ac1abda --- /dev/null +++ b/.squad/templates/skills/gh-auth-isolation/SKILL.md @@ -0,0 +1,183 @@ +--- +name: "gh-auth-isolation" +description: "Safely manage multiple GitHub identities (EMU + personal) in agent workflows" +domain: "security, github-integration, authentication, multi-account" +confidence: "high" +source: "earned (production usage across 50+ sessions with EMU corp + personal GitHub accounts)" +tools: + - name: "gh" + description: "GitHub CLI for authenticated operations" + when: "When accessing GitHub resources requiring authentication" +--- + +## Context + +Many developers use GitHub through an Enterprise Managed User (EMU) account at work while maintaining a personal GitHub account for open-source contributions. AI agents spawned by Squad inherit the shell's default `gh` authentication — which is usually the EMU account. This causes failures when agents try to push to personal repos, create PRs on forks, or interact with resources outside the enterprise org. + +This skill teaches agents how to detect the active identity, switch contexts safely, and avoid mixing credentials across operations. + +## Patterns + +### Detect Current Identity + +Before any GitHub operation, check which account is active: + +```bash +gh auth status +``` + +Look for: +- `Logged in to github.com as USERNAME` — the active account +- `Token scopes: ...` — what permissions are available +- Multiple accounts will show separate entries + +### Extract a Specific Account's Token + +When you need to operate as a specific user (not the default): + +```bash +# Get the personal account token (by username) +gh auth token --user personaluser + +# Get the EMU account token +gh auth token --user corpalias_enterprise +``` + +**Use case:** Push to a personal fork while the default `gh` auth is the EMU account. + +### Push to Personal Repos from EMU Shell + +The most common scenario: your shell defaults to the EMU account, but you need to push to a personal GitHub repo. + +```bash +# 1. Extract the personal token +$token = gh auth token --user personaluser + +# 2. Push using token-authenticated HTTPS +git push https://personaluser:$token@github.com/personaluser/repo.git branch-name +``` + +**Why this works:** `gh auth token --user` reads from `gh`'s credential store without switching the active account. The token is used inline for a single operation and never persisted. + +### Create PRs on Personal Forks + +When the default `gh` context is EMU but you need to create a PR from a personal fork: + +```bash +# Option 1: Use --repo flag (works if token has access) +gh pr create --repo upstream/repo --head personaluser:branch --title "..." --body "..." + +# Option 2: Temporarily set GH_TOKEN for one command +$env:GH_TOKEN = $(gh auth token --user personaluser) +gh pr create --repo upstream/repo --head personaluser:branch --title "..." +Remove-Item Env:\GH_TOKEN +``` + +### Config Directory Isolation (Advanced) + +For complete isolation between accounts, use separate `gh` config directories: + +```bash +# Personal account operations +$env:GH_CONFIG_DIR = "$HOME/.config/gh-public" +gh auth login # Login with personal account (one-time setup) +gh repo clone personaluser/repo + +# EMU account operations (default) +Remove-Item Env:\GH_CONFIG_DIR +gh auth status # Back to EMU account +``` + +**Setup (one-time):** +```bash +# Create isolated config for personal account +mkdir ~/.config/gh-public +$env:GH_CONFIG_DIR = "$HOME/.config/gh-public" +gh auth login --web --git-protocol https +``` + +### Shell Aliases for Quick Switching + +Add to your shell profile for convenience: + +```powershell +# PowerShell profile +function ghp { $env:GH_CONFIG_DIR = "$HOME/.config/gh-public"; gh @args; Remove-Item Env:\GH_CONFIG_DIR } +function ghe { gh @args } # Default EMU + +# Usage: +# ghp repo clone personaluser/repo # Uses personal account +# ghe issue list # Uses EMU account +``` + +```bash +# Bash/Zsh profile +alias ghp='GH_CONFIG_DIR=~/.config/gh-public gh' +alias ghe='gh' + +# Usage: +# ghp repo clone personaluser/repo +# ghe issue list +``` + +## Examples + +### āœ“ Correct: Agent pushes blog post to personal GitHub Pages + +```powershell +# Agent needs to push to personaluser.github.io (personal repo) +# Default gh auth is corpalias_enterprise (EMU) + +$token = gh auth token --user personaluser +git remote set-url origin https://personaluser:$token@github.com/personaluser/personaluser.github.io.git +git push origin main + +# Clean up — don't leave token in remote URL +git remote set-url origin https://github.com/personaluser/personaluser.github.io.git +``` + +### āœ“ Correct: Agent creates a PR from personal fork to upstream + +```powershell +# Fork: personaluser/squad, Upstream: bradygaster/squad +# Agent is on branch contrib/fix-docs in the fork clone + +git push origin contrib/fix-docs # Pushes to fork (may need token auth) + +# Create PR targeting upstream +gh pr create --repo bradygaster/squad --head personaluser:contrib/fix-docs ` + --title "docs: fix installation guide" ` + --body "Fixes #123" +``` + +### āœ— Incorrect: Blindly pushing with wrong account + +```bash +# BAD: Agent assumes default gh auth works for personal repos +git push origin main +# ERROR: Permission denied — EMU account has no access to personal repo + +# BAD: Hardcoding tokens in scripts +git push https://personaluser:ghp_xxxxxxxxxxxx@github.com/personaluser/repo.git main +# SECURITY RISK: Token exposed in command history and process list +``` + +### āœ“ Correct: Check before you push + +```bash +# Always verify which account has access before operations +gh auth status +# If wrong account, use token extraction: +$token = gh auth token --user personaluser +git push https://personaluser:$token@github.com/personaluser/repo.git main +``` + +## Anti-Patterns + +- āŒ **Hardcoding tokens** in scripts, environment variables, or committed files. Use `gh auth token --user` to extract at runtime. +- āŒ **Assuming the default `gh` auth works** for all repos. EMU accounts can't access personal repos and vice versa. +- āŒ **Switching `gh auth login`** globally mid-session. This changes the default for ALL processes and can break parallel agents. +- āŒ **Storing personal tokens in `.env`** or `.squad/` files. These get committed by Scribe. Use `gh`'s credential store. +- āŒ **Ignoring token cleanup** after inline HTTPS pushes. Always reset the remote URL to avoid persisting tokens. +- āŒ **Using `gh auth switch`** in multi-agent sessions. One agent switching affects all others sharing the shell. +- āŒ **Mixing EMU and personal operations** in the same git clone. Use separate clones or explicit remote URLs per operation. diff --git a/.squad/templates/skills/git-workflow/SKILL.md b/.squad/templates/skills/git-workflow/SKILL.md new file mode 100644 index 000000000..bfa0b8596 --- /dev/null +++ b/.squad/templates/skills/git-workflow/SKILL.md @@ -0,0 +1,204 @@ +--- +name: "git-workflow" +description: "Squad branching model: dev-first workflow with insiders preview channel" +domain: "version-control" +confidence: "high" +source: "team-decision" +--- + +## Context + +Squad uses a three-branch model. **All feature work starts from `dev`, not `main`.** + +| Branch | Purpose | Publishes | +|--------|---------|-----------| +| `main` | Released, tagged, in-npm code only | `npm publish` on tag | +| `dev` | Integration branch — all feature work lands here | `npm publish --tag preview` on merge | +| `insiders` | Early-access channel — synced from dev | `npm publish --tag insiders` on sync | + +## Branch Naming Convention + +Issue branches MUST use: `squad/{issue-number}-{kebab-case-slug}` + +Examples: +- `squad/195-fix-version-stamp-bug` +- `squad/42-add-profile-api` + +## Workflow for Issue Work + +1. **Branch from dev:** + ```bash + git checkout dev + git pull origin dev + git checkout -b squad/{issue-number}-{slug} + ``` + +2. **Mark issue in-progress:** + ```bash + gh issue edit {number} --add-label "status:in-progress" + ``` + +3. **Create draft PR targeting dev:** + ```bash + gh pr create --base dev --title "{description}" --body "Closes #{issue-number}" --draft + ``` + +4. **Do the work.** Make changes, write tests, commit with issue reference. + +5. **Push and mark ready:** + ```bash + git push -u origin squad/{issue-number}-{slug} + gh pr ready + ``` + +6. **After merge to dev:** + ```bash + git checkout dev + git pull origin dev + git branch -d squad/{issue-number}-{slug} + git push origin --delete squad/{issue-number}-{slug} + ``` + +## Parallel Multi-Issue Work (Worktrees) + +When the coordinator routes multiple issues simultaneously (e.g., "fix bugs X, Y, and Z"), use `git worktree` to give each agent an isolated working directory. No filesystem collisions, no branch-switching overhead. + +### When to Use Worktrees vs Sequential + +| Scenario | Strategy | +|----------|----------| +| Single issue | Standard workflow above — no worktree needed | +| 2+ simultaneous issues in same repo | Worktrees — one per issue | +| Work spanning multiple repos | Separate clones as siblings (see Multi-Repo below) | + +### Setup + +From the main clone (must be on dev or any branch): + +```bash +# Ensure dev is current +git fetch origin dev + +# Create a worktree per issue — siblings to the main clone +git worktree add ../squad-195 -b squad/195-fix-stamp-bug origin/dev +git worktree add ../squad-193 -b squad/193-refactor-loader origin/dev +``` + +**Naming convention:** `../{repo-name}-{issue-number}` (e.g., `../squad-195`, `../squad-pr-42`). + +Each worktree: +- Has its own working directory and index +- Is on its own `squad/{issue-number}-{slug}` branch from dev +- Shares the same `.git` object store (disk-efficient) + +### Per-Worktree Agent Workflow + +Each agent operates inside its worktree exactly like the single-issue workflow: + +```bash +cd ../squad-195 + +# Work normally — commits, tests, pushes +git add -A && git commit -m "fix: stamp bug (#195)" +git push -u origin squad/195-fix-stamp-bug + +# Create PR targeting dev +gh pr create --base dev --title "fix: stamp bug" --body "Closes #195" --draft +``` + +All PRs target `dev` independently. Agents never interfere with each other's filesystem. + +### .squad/ State in Worktrees + +The `.squad/` directory exists in each worktree as a copy. This is safe because: +- `.gitattributes` declares `merge=union` on append-only files (history.md, decisions.md, logs) +- Each agent appends to its own section; union merge reconciles on PR merge to dev +- **Rule:** Never rewrite or reorder `.squad/` files in a worktree — append only + +### Cleanup After Merge + +After a worktree's PR is merged to dev: + +```bash +# From the main clone +git worktree remove ../squad-195 +git worktree prune # clean stale metadata +git branch -d squad/195-fix-stamp-bug +git push origin --delete squad/195-fix-stamp-bug +``` + +If a worktree was deleted manually (rm -rf), `git worktree prune` recovers the state. + +--- + +## Multi-Repo Downstream Scenarios + +When work spans multiple repositories (e.g., squad-cli changes need squad-sdk changes, or a user's app depends on squad): + +### Setup + +Clone downstream repos as siblings to the main repo: + +``` +~/work/ + squad-pr/ # main repo + squad-sdk/ # downstream dependency + user-app/ # consumer project +``` + +Each repo gets its own issue branch following its own naming convention. If the downstream repo also uses Squad conventions, use `squad/{issue-number}-{slug}`. + +### Coordinated PRs + +- Create PRs in each repo independently +- Link them in PR descriptions: + ``` + Closes #42 + + **Depends on:** squad-sdk PR #17 (squad-sdk changes required for this feature) + ``` +- Merge order: dependencies first (e.g., squad-sdk), then dependents (e.g., squad-cli) + +### Local Linking for Testing + +Before pushing, verify cross-repo changes work together: + +```bash +# Node.js / npm +cd ../squad-sdk && npm link +cd ../squad-pr && npm link squad-sdk + +# Go +# Use replace directive in go.mod: +# replace github.com/org/squad-sdk => ../squad-sdk + +# Python +cd ../squad-sdk && pip install -e . +``` + +**Important:** Remove local links before committing. `npm link` and `go replace` are dev-only — CI must use published packages or PR-specific refs. + +### Worktrees + Multi-Repo + +These compose naturally. You can have: +- Multiple worktrees in the main repo (parallel issues) +- Separate clones for downstream repos +- Each combination operates independently + +--- + +## Anti-Patterns + +- āŒ Branching from main (branch from dev) +- āŒ PR targeting main directly (target dev) +- āŒ Non-conforming branch names (must be squad/{number}-{slug}) +- āŒ Committing directly to main or dev (use PRs) +- āŒ Switching branches in the main clone while worktrees are active (use worktrees instead) +- āŒ Using worktrees for cross-repo work (use separate clones) +- āŒ Leaving stale worktrees after PR merge (clean up immediately) + +## Promotion Pipeline + +- dev → insiders: Automated sync on green build +- dev → main: Manual merge when ready for stable release, then tag +- Hotfixes: Branch from main as `hotfix/{slug}`, PR to dev, cherry-pick to main if urgent diff --git a/.squad/templates/skills/github-multi-account/SKILL.md b/.squad/templates/skills/github-multi-account/SKILL.md new file mode 100644 index 000000000..0a2158f33 --- /dev/null +++ b/.squad/templates/skills/github-multi-account/SKILL.md @@ -0,0 +1,95 @@ +--- +name: github-multi-account +description: Detect and set up account-locked gh aliases for multi-account GitHub. The AI reads this skill, detects accounts, asks the user which is personal/work, and runs the setup automatically. +confidence: high +source: https://github.com/tamirdresher/squad-skills/tree/main/plugins/github-multi-account +author: tamirdresher +--- + +# GitHub Multi-Account — AI-Driven Setup + +## When to Activate +When the user has multiple GitHub accounts (check with `gh auth status`). If you see 2+ accounts listed, this skill applies. + +## What to Do (as the AI agent) + +### Step 1: Detect accounts +Run: `gh auth status` +Look for multiple accounts. Note which usernames are listed. + +### Step 2: Ask the user +Ask: "I see you have multiple GitHub accounts: {list them}. Which one is your personal account and which is your work/EMU account?" + +### Step 3: Run the setup automatically +Once the user confirms, do ALL of this for them: + +```powershell +# 1. Define the functions +$personal = "THEIR_PERSONAL_USERNAME" +$work = "THEIR_WORK_USERNAME" + +# 2. Add to PowerShell profile +$profilePath = $PROFILE.CurrentUserAllHosts +if (!(Test-Path $profilePath)) { New-Item -Path $profilePath -Force | Out-Null } +$existing = Get-Content $profilePath -Raw -ErrorAction SilentlyContinue +if ($existing -notmatch "gh-personal") { + $block = @" + +# === GitHub Multi-Account Aliases === +function gh-personal { gh auth switch --user $personal 2>`$null | Out-Null; gh @args } +function gh-work { gh auth switch --user $work 2>`$null | Out-Null; gh @args } +Set-Alias ghp gh-personal +Set-Alias ghw gh-work +"@ + Add-Content -Path $profilePath -Value $block +} + +# 3. Create CMD wrappers +$binDir = Join-Path $env:USERPROFILE ".squad\bin" +if (!(Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null } +"@echo off`ngh auth switch --user $personal >nul 2>&1`ngh %*" | Out-File "$binDir\ghp.cmd" -Encoding ascii +"@echo off`ngh auth switch --user $work >nul 2>&1`ngh %*" | Out-File "$binDir\ghw.cmd" -Encoding ascii + +# 4. Add to PATH +$userPath = [Environment]::GetEnvironmentVariable("PATH", "User") +if ($userPath -notmatch [regex]::Escape($binDir)) { + [Environment]::SetEnvironmentVariable("PATH", "$binDir;$userPath", "User") + $env:PATH = "$binDir;$env:PATH" +} + +# 5. Load in current session +function gh-personal { gh auth switch --user $personal 2>$null | Out-Null; gh @args } +function gh-work { gh auth switch --user $work 2>$null | Out-Null; gh @args } +Set-Alias ghp gh-personal +Set-Alias ghw gh-work +``` + +### Step 4: Verify for the user +Run these and show the results: +```powershell +ghp api user --jq '.login' # should show personal username +ghw api user --jq '.login' # should show work username +``` + +### Step 5: Tell the user +"All set! From now on use `ghp` for personal repos and `ghw` for work repos. I'll use them too." + +## After Setup — Usage Rules + +1. **NEVER** use bare `gh` for repo operations — always `ghp` or `ghw` +2. **NEVER** manually `gh auth switch` — the aliases handle it +3. Determine alias by repo owner: + - Personal account repos → `ghp` / `gh-personal` + - Work/EMU account repos → `ghw` / `gh-work` + +## Repo-Specific Account Binding + +This repo (`bradygaster/squad`) is bound to the **bradygaster** (personal) account. +All `gh` operations in this repo MUST use `ghp` / `gh-personal`. + +## For Squad Agents +At the TOP of any script touching GitHub, define: +```powershell +function gh-personal { gh auth switch --user bradygaster 2>$null | Out-Null; gh @args } +function gh-work { gh auth switch --user bradyg_microsoft 2>$null | Out-Null; gh @args } +``` diff --git a/.squad/templates/skills/history-hygiene/SKILL.md b/.squad/templates/skills/history-hygiene/SKILL.md new file mode 100644 index 000000000..453a03b4e --- /dev/null +++ b/.squad/templates/skills/history-hygiene/SKILL.md @@ -0,0 +1,36 @@ +--- +name: history-hygiene +description: Record final outcomes to history.md, not intermediate requests or reversed decisions +domain: documentation, team-collaboration +confidence: high +source: earned (Kobayashi v0.6.0 incident, team intervention) +--- + +## Context + +History files (.md files tracking decisions, spawns, outcomes) are read cold by future agents. Stale or incorrect entries poison decision-making downstream. The Kobayashi incident proved this: history said "Brady decided v0.6.0" when Brady had reversed that to v0.8.17. Future spawns read the wrong truth and repeated the mistake. + +## Patterns + +- **Record the final outcome**, not the initial request. +- **Wait for confirmation** before writing to history — don't log intermediate states. +- **If a decision reverses**, update the entry immediately — don't leave stale data. +- **One read = one truth.** A future agent should never need to cross-reference other files to understand what actually happened. + +## Examples + +āœ“ **Correct:** +- "Migration target: v0.8.17 (initially discussed as v0.6.0, corrected by Brady)" +- "Reverted to Node 18 per Brady's explicit request on 2024-01-15" + +āœ— **Incorrect:** +- "Brady directed v0.6.0" (when later reversed) +- Recording what was *requested* instead of what *actually happened* +- Logging entries before outcome is confirmed + +## Anti-Patterns + +- Writing intermediate or "for now" states to disk +- Attributing decisions without confirming final direction +- Treating history like a draft — history is the source of truth +- Assuming readers will cross-reference or verify; they won't diff --git a/.squad/templates/skills/humanizer/SKILL.md b/.squad/templates/skills/humanizer/SKILL.md new file mode 100644 index 000000000..4dbb854df --- /dev/null +++ b/.squad/templates/skills/humanizer/SKILL.md @@ -0,0 +1,105 @@ +--- +name: "humanizer" +description: "Tone enforcement patterns for external-facing community responses" +domain: "communication, tone, community" +confidence: "low" +source: "manual (RFC #426 — PAO External Communications)" +--- + +## Context + +Use this skill whenever PAO drafts external-facing responses for issues or discussions. + +- Tone must be warm, helpful, and human-sounding — never robotic or corporate. +- Brady's constraint applies everywhere: **Humanized tone is mandatory**. +- This applies to **all external-facing content** drafted by PAO in Phase 1 issues/discussions workflows. + +## Patterns + +1. **Warm opening** — Start with acknowledgment ("Thanks for reporting this", "Great question!") +2. **Active voice** — "We're looking into this" not "This is being investigated" +3. **Second person** — Address the person directly ("you" not "the user") +4. **Conversational connectors** — "That said...", "Here's what we found...", "Quick note:" +5. **Specific, not vague** — "This affects the casting module in v0.8.x" not "We are aware of issues" +6. **Empathy markers** — "I can see how that would be frustrating", "Good catch!" +7. **Action-oriented closes** — "Let us know if that helps!" not "Please advise if further assistance is required" +8. **Uncertainty is OK** — "We're not 100% sure yet, but here's what we think is happening..." is better than false confidence +9. **Profanity filter** — Never include profanity, slurs, or aggressive language, even when quoting +10. **Baseline comparison** — Responses should align with tone of 5-10 "gold standard" responses (>80% similarity threshold) +11. **Empathetic disagreement** — "We hear you. That's a fair concern." before explaining the reasoning +12. **Information request** — Ask for specific details, not open-ended "can you provide more info?" +13. **No link-dumping** — Don't just paste URLs. Provide context: "Check out the [getting started guide](url) — specifically the section on routing" not just a bare link + +## Examples + +### 1. Welcome + +```text +Hey {author}! Welcome to Squad šŸ‘‹ Thanks for opening this. +{substantive response} +Let us know if you have questions — happy to help! +``` + +### 2. Troubleshooting + +```text +Thanks for the detailed report, {author}! +Here's what we think is happening: {explanation} +{steps or workaround} +Let us know if that helps, or if you're seeing something different. +``` + +### 3. Feature guidance + +```text +Great question! {context on current state} +{guidance or workaround} +We've noted this as a potential improvement — {tracking info if applicable}. +``` + +### 4. Redirect + +```text +Thanks for reaching out! This one is actually better suited for {correct location}. +{brief explanation of why} +Feel free to open it there — they'll be able to help! +``` + +### 5. Acknowledgment + +```text +Good catch, {author}. We've confirmed this is a real issue. +{what we know so far} +We'll update this thread when we have a fix. Thanks for flagging it! +``` + +### 6. Closing + +```text +This should be resolved in {version/PR}! šŸŽ‰ +{brief summary of what changed} +Thanks for reporting this, {author} — it made Squad better. +``` + +### 7. Technical uncertainty + +```text +Interesting find, {author}. We're not 100% sure what's causing this yet. +Here's what we've ruled out: {list} +We'd love more context if you have it — {specific ask}. +We'll dig deeper and update this thread. +``` + +## Anti-Patterns + +- āŒ Corporate speak: "We appreciate your patience as we investigate this matter" +- āŒ Marketing hype: "Squad is the BEST way to..." or "This amazing feature..." +- āŒ Passive voice: "It has been determined that..." or "The issue is being tracked" +- āŒ Dismissive: "This works as designed" without empathy +- āŒ Over-promising: "We'll ship this next week" without commitment from the team +- āŒ Empty acknowledgment: "Thanks for your feedback" with no substance +- āŒ Robot signatures: "Best regards, PAO" or "Sincerely, The Squad Team" +- āŒ Excessive emoji: More than 1-2 emoji per response +- āŒ Quoting profanity: Even when the original issue contains it, paraphrase instead +- āŒ Link-dumping: Pasting URLs without context ("See: https://...") +- āŒ Open-ended info requests: "Can you provide more information?" without specifying what information diff --git a/.squad/templates/skills/init-mode/SKILL.md b/.squad/templates/skills/init-mode/SKILL.md new file mode 100644 index 000000000..430c6ae15 --- /dev/null +++ b/.squad/templates/skills/init-mode/SKILL.md @@ -0,0 +1,102 @@ +--- +name: "init-mode" +description: "Team initialization flow (Phase 1 proposal + Phase 2 creation)" +domain: "orchestration" +confidence: "high" +source: "extracted" +tools: + - name: "ask_user" + description: "Confirm team roster with selectable menu" + when: "Phase 1 proposal — requires explicit user confirmation" +--- + +## Context + +Init Mode activates when `.squad/team.md` does not exist, or exists but has zero roster entries under `## Members`. The coordinator proposes a team (Phase 1), waits for user confirmation, then creates the team structure (Phase 2). + +## Patterns + +### Phase 1: Propose the Team + +No team exists yet. Propose one — but **DO NOT create any files until the user confirms.** + +1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey {user}, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** +2. Ask: *"What are you building? (language, stack, what it does)"* +3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): + - Determine team size (typically 4–5 + Scribe). + - Determine assignment shape from the user's project description. + - Derive resonance signals from the session and repo context. + - Select a universe. If the universe is custom, allocate character names from that universe based on the related list found in the `.squad/templates/casting/` directory. Prefer custom universes when available. + - Scribe is always "Scribe" — exempt from casting. + - Ralph is always "Ralph" — exempt from casting. +4. Propose the team with their cast names. Example (names will vary per cast): + +``` +šŸ—ļø {CastName1} — Lead Scope, decisions, code review +āš›ļø {CastName2} — Frontend Dev React, UI, components +šŸ”§ {CastName3} — Backend Dev APIs, database, services +🧪 {CastName4} — Tester Tests, quality, edge cases +šŸ“‹ Scribe — (silent) Memory, decisions, session logs +šŸ”„ Ralph — (monitor) Work queue, backlog, keep-alive +``` + +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: + - **question:** *"Look right?"* + - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` + +**āš ļø STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** + +### Phase 2: Create the Team + +**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). + +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. + +6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). + +**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). + +**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. + +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. + +**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: +``` +.squad/decisions.md merge=union +.squad/agents/*/history.md merge=union +.squad/log/** merge=union +.squad/orchestration-log/** merge=union +``` +The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically. + +7. Say: *"āœ… Team hired. Try: '{FirstCastName}, set up the project structure'"* + +8. **Post-setup input sources** (optional — ask after team is created, not during casting): + - PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow + - GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow + - Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section + - Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment + - These are additive. Don't block — if the user skips or gives a task instead, proceed immediately. + +## Examples + +**Example flow:** +1. Coordinator detects no team.md → Init Mode +2. Runs `git config user.name` → "{user}" +3. Asks: *"Hey {user}, what are you building?"* +4. User: *"TypeScript CLI tool with GitHub API integration"* +5. Coordinator runs casting algorithm → selects "The Usual Suspects" universe +6. Proposes: Keaton (Lead), Verbal (Prompt), Fenster (Backend), Hockney (Tester), Scribe, Ralph +7. Uses `ask_user` with choices → user selects "Yes, hire this team" +8. Coordinator creates `.squad/` structure, initializes casting state, seeds agents +9. Says: *"āœ… Team hired. Try: 'Keaton, set up the project structure'"* + +## Anti-Patterns + +- āŒ Creating files before user confirms Phase 1 +- āŒ Mixing agents from different universes in the same cast +- āŒ Skipping the `ask_user` tool and assuming confirmation +- āŒ Proceeding to Phase 2 when user said "add someone" or "change a role" +- āŒ Using `## Team Roster` instead of `## Members` as the header (breaks GitHub workflows) +- āŒ Forgetting to initialize `.squad/casting/` state files +- āŒ Reading or storing `git config user.email` (PII violation) diff --git a/.squad/templates/skills/iterative-retrieval/SKILL.md b/.squad/templates/skills/iterative-retrieval/SKILL.md new file mode 100644 index 000000000..4d8eea993 --- /dev/null +++ b/.squad/templates/skills/iterative-retrieval/SKILL.md @@ -0,0 +1,165 @@ +--- +name: "iterative-retrieval" +description: "Max-3-cycle protocol for agent sub-tasks with WHY context and coordinator validation. Use when spawning sub-agents to complete scoped work." +domain: "agent-coordination" +confidence: "high" +license: MIT +--- + +# Iterative Retrieval Skill + +Squad agents frequently spawn sub-agents to complete scoped work. Without structure, these +handoffs become vague, cycles multiply, and outputs land without being checked. The +**Iterative Retrieval Pattern** caps cycles at 3, mandates WHY context in every spawn, and +requires the coordinator to validate agent output before closing an issue. + +--- + +## Spawn Prompt Template + +Every agent spawn must include the following four sections. Copy and fill in the template: + +``` +## Task +{What you need done — concrete and bounded} + +## WHY this matters +{The motivation and context. What system or user goal does this serve? What breaks if skipped?} + +## Success criteria +{How you will know the output is correct. Be explicit — list acceptance criteria, not vibes.} +Example: +- [ ] File X exists and contains Y +- [ ] No regressions in existing tests +- [ ] PR is open targeting main with description matching the issue + +## Escalation path +{What the agent should do if uncertain or stuck. "Stop and ask me" is valid.} +Example: +- If requirements are ambiguous → stop, comment on the issue, set label status:needs-decision +- If blocked by a dependency → label status:blocked, explain in a comment +- If 3 cycles exhausted without resolution → write a summary to inbox and surface to coordinator +``` + +--- + +## 3-Cycle Protocol + +| Cycle | Description | Exit condition | +|-------|-------------|----------------| +| **1** | Initial attempt | Done → coordinator validates. Incomplete → surface delta. | +| **2** | Targeted retry with specific corrections | Done → coordinator validates. Incomplete → one more. | +| **3** | Final attempt with all context from cycles 1–2 | Done or escalate — no cycle 4. | + +### Rules + +1. **After each cycle**, the coordinator evaluates the output against the success criteria + before accepting it or spawning the next cycle. +2. **Objective context forward**: each subsequent spawn includes a summary of what was tried + and what is still missing — not just a repeat of the original task. +3. **Cycle 3 exhausted** → escalate: write a summary to `.squad/decisions/inbox/`, label the + issue `status:needs-decision`, and notify the user. + +--- + +## Coordinator Validation Checklist + +Before accepting agent output and closing an issue, the coordinator must check: + +- [ ] All success criteria from the spawn prompt are met +- [ ] PR exists and description matches the issue (if code work) +- [ ] No obvious regressions (grep for TODO/FIXME introduced, build passes) +- [ ] Agent did not silently skip parts of the task +- [ ] If the agent reported uncertainty — was it resolved or escalated? + +If any item fails → do **not** accept. Spawn cycle N+1 (up to cycle 3) with specific deltas. + +--- + +## When to Escalate vs Retry + +**Retry (cycle N+1)** when: +- Output is structurally correct but missing specific items +- Agent misunderstood scope (provide more context and re-run) +- Partial success — clearly identified remaining delta + +**Escalate** when: +- Requirements are fundamentally unclear (decision needed) +- 3 cycles complete without convergence +- Agent returned conflicting results across cycles +- Task requires elevated permissions or external action +- The work depends on another issue that isn't done yet + +--- + +## Issue Dedup Check (Mandatory) + +Before any agent creates a GitHub issue, it **must** search for existing open issues to avoid +duplicates. + +```bash +# Check for existing open issues before creating a new one +gh issue list --search "" --state open +``` + +- If an open issue already covers the same problem → **comment on it** instead of creating a new one. +- If no duplicate → proceed to create the issue. +- Use 2–3 representative keywords from the planned issue title as the search query. + +--- + +## Mandatory Output Requirement (Research-Then-Execute) + +Every research or analysis task completed under this protocol **MUST** end with at least one +concrete action before the cycle is closed. Acceptable follow-up actions: + +- GitHub issue created documenting the findings and next steps +- PR opened implementing a recommendation +- Decision recorded in `.squad/decisions/inbox/` +- Documented recommendation with a named assignee and due date + +**Pure analysis reports without actionable follow-up will be rejected during triage.** +If no action is warranted, the agent must explicitly state why and get coordinator sign-off. + +--- + +## Anti-Patterns + +- **Spawning without WHY** — agents can't prioritise trade-offs without motivation context. +- **Accepting output without validating** — one failed check avoids merging broken work. +- **Cycle 4+** — if 3 cycles haven't converged, the problem is in the requirements, not the agent. +- **Vague success criteria** — "looks good" is not a criterion. Use checkboxes. +- **Forwarding WHAT without delta** — cycle 2+ prompts must include what cycle 1 got wrong. +- **Creating issues without dedup check** — always search before creating. +- **Research without action** — delivering analysis with no issue, PR, decision, or assignee is incomplete work. + +--- + +## Examples + +### Good spawn prompt +``` +## Task +Add an "Iterative Retrieval Protocol" section to `.squad/agents/coordinator/charter.md` explaining +the 3-cycle rule, WHY format, and validation checklist. + +## WHY this matters +The coordinator spawns sub-agents on every round. Without a documented protocol, agents run unbounded +cycles and outputs go unvalidated — leading to stale issues and silent failures. + +## Success criteria +- [ ] Section "Iterative Retrieval Protocol" exists in charter.md +- [ ] Section documents max-3-cycles rule +- [ ] Section documents WHY format requirement +- [ ] Section contains validation checklist (at least 4 items) +- [ ] No other sections of charter.md are modified + +## Escalation path +If the charter.md format is unclear, check another agent charter as a reference. +If uncertain about content, stop and surface to coordinator. +``` + +### Bad spawn prompt (don't do this) +``` +Update the coordinator charter with the iterative retrieval stuff. +``` diff --git a/.squad/templates/skills/model-selection/SKILL.md b/.squad/templates/skills/model-selection/SKILL.md new file mode 100644 index 000000000..9842616e5 --- /dev/null +++ b/.squad/templates/skills/model-selection/SKILL.md @@ -0,0 +1,125 @@ +--- +name: "model-selection" +description: "Determines which LLM model to use for each agent spawn" +domain: "orchestration" +confidence: "medium" +source: "extracted" +--- + +# Model Selection + +> Determines which LLM model to use for each agent spawn. + +## SCOPE + +āœ… THIS SKILL PRODUCES: +- A resolved `model` parameter for every `task` tool call +- Persistent model preferences in `.squad/config.json` +- Spawn acknowledgments that include the resolved model + +āŒ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Model performance benchmarks +- Cost reports or billing artifacts + +## Context + +Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions. + +## 5-Layer Model Resolution Hierarchy + +Resolution is **first-match-wins** — the highest layer with a value wins. + +| Layer | Name | Source | Persistence | +|-------|------|--------|-------------| +| **0a** | Per-Agent Config | `.squad/config.json` → `agentModelOverrides.{name}` | Persistent (survives sessions) | +| **0b** | Global Config | `.squad/config.json` → `defaultModel` | Persistent (survives sessions) | +| **1** | Session Directive | User said "use X" in current session | Session-only | +| **2** | Charter Preference | Agent's `charter.md` → `## Model` section | Persistent (in charter) | +| **3** | Task-Aware Auto | Code → sonnet, docs → haiku, visual → opus | Computed per-spawn | +| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback | + +**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional — the user explicitly chose quality over cost. + +## AGENT WORKFLOW + +### On Session Start + +1. READ `.squad/config.json` +2. CHECK for `defaultModel` field — if present, this is the Layer 0 override for all spawns +3. CHECK for `agentModelOverrides` field — if present, these are per-agent Layer 0a overrides +4. STORE both values in session context for the duration + +### On Every Agent Spawn + +1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? → Use it. +2. CHECK Layer 0b: Is there a `defaultModel` in config.json? → Use it. +3. CHECK Layer 1: Did the user give a session directive? → Use it. +4. CHECK Layer 2: Does the agent's charter have a `## Model` section? → Use it. +5. CHECK Layer 3: Determine task type: + - Code (implementation, tests, refactoring, bug fixes) → `claude-sonnet-4.6` + - Prompts, agent designs → `claude-sonnet-4.6` + - Visual/design with image analysis → `claude-opus-4.6` + - Non-code (docs, planning, triage, changelogs) → `claude-haiku-4.5` +6. FALLBACK Layer 4: `claude-haiku-4.5` +7. INCLUDE model in spawn acknowledgment: `šŸ”§ {Name} ({resolved_model}) — {task}` + +### When User Sets a Preference + +**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X" + +1. VALIDATE the model ID against the catalog (18+ models) +2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite) +3. ACKNOWLEDGE: `āœ… Model preference saved: {model} — all future sessions will use this until changed.` + +**Per-agent trigger:** "use X for {agent}" + +1. VALIDATE model ID +2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json` +3. ACKNOWLEDGE: `āœ… {Agent} will always use {model} — saved to config.` + +### When User Clears a Preference + +**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models" + +1. REMOVE `defaultModel` from `.squad/config.json` +2. ACKNOWLEDGE: `āœ… Model preference cleared — returning to automatic selection.` + +### STOP + +After resolving the model and including it in the spawn template, this skill is done. Do NOT: +- Generate model comparison reports +- Run benchmarks or speed tests +- Create new config files (only modify existing `.squad/config.json`) +- Change the model after spawn (fallback chains handle runtime failures) + +## Config Schema + +`.squad/config.json` model-related fields: + +```json +{ + "version": 1, + "defaultModel": "claude-opus-4.6", + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6", + "mcmanus": "claude-haiku-4.5" + } +} +``` + +- `defaultModel` — applies to ALL agents unless overridden by `agentModelOverrides` +- `agentModelOverrides` — per-agent overrides that take priority over `defaultModel` +- Both fields are optional. When absent, Layers 1-4 apply normally. + +## Fallback Chains + +If a model is unavailable (rate limit, plan restriction), retry within the same tier: + +``` +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 +Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 +Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini +``` + +**Never fall UP in tier.** A fast task won't land on a premium model via fallback. diff --git a/.squad/templates/skills/nap/SKILL.md b/.squad/templates/skills/nap/SKILL.md new file mode 100644 index 000000000..051cf4b45 --- /dev/null +++ b/.squad/templates/skills/nap/SKILL.md @@ -0,0 +1,32 @@ +--- +name: "nap" +description: "Context hygiene — compress, prune, archive .squad/ state" +domain: "maintenance" +confidence: "medium" +source: "extracted" +--- + +# Skill: nap + +> Context hygiene — compress, prune, archive .squad/ state + +## What It Does + +Reclaims context window budget by compressing agent histories, pruning old logs, +archiving stale decisions, and cleaning orphaned inbox files. + +## When To Use + +- Before heavy fan-out work (many agents will spawn) +- When history.md files exceed 15KB +- When .squad/ total size exceeds 1MB +- After long-running sessions or sprints + +## Invocation + +- CLI: `squad nap` / `squad nap --deep` / `squad nap --dry-run` +- REPL: `/nap` / `/nap --dry-run` / `/nap --deep` + +## Confidence + +medium — Confirmed by team vote (4-1) and initial implementation diff --git a/.squad/templates/skills/notification-routing/SKILL.md b/.squad/templates/skills/notification-routing/SKILL.md new file mode 100644 index 000000000..2b2df1fec --- /dev/null +++ b/.squad/templates/skills/notification-routing/SKILL.md @@ -0,0 +1,105 @@ +--- +name: "notification-routing" +description: "Route agent notifications to specific channels by type — prevent alert fatigue from single-channel flooding" +domain: "communication" +confidence: "high" +source: "earned" +--- + +## Context + +When a Squad grows beyond a few agents, notifications flood a single channel — failure alerts drown in daily +briefings, tech news buries security findings, and everything gets ignored. This is the pub-sub problem: +a single message queue for everything is a recipe for missed alerts. + +The fix is **topic-based routing**: agents tag notifications with a channel type, and a routing function +sends them to the appropriate destination. + +**Trigger symptoms:** +- Important alerts missed because they're buried in routine notifications +- Team members turning off notifications entirely (signal overwhelm) +- Onboarding friction: "where do I look for X?" + +## Patterns + +### Channel Config Schema + +Define a `.squad/teams-channels.json` (or equivalent) mapping notification types to channel identifiers: + +```json +{ + "teamId": "your-team-id", + "channels": { + "notifications": "squad-alerts", + "tech-news": "tech-news", + "security": "security-findings", + "releases": "release-announcements", + "daily-digest": "daily-digest" + } +} +``` + +Place this in `.squad/` (git-tracked, shared across the team). For platforms that use channel IDs instead of +names (Teams, Slack), store the resolved ID alongside the name to avoid name-collision bugs: + +```json +{ + "channels": { + "notifications": { "name": "squad-alerts", "id": "channel-id-opaque-string" } + } +} +``` + +### CHANNEL: Tag Convention + +Agents prefix their output with `CHANNEL:` to signal where the notification should go: + +``` +CHANNEL:security +Worf found 3 new CVEs in dependency scan: lodash@4.17.15, minimist@1.2.5 +``` + +### Routing Dispatcher (shell pseudocode) + +```bash +dispatch_notification() { + local raw_output="$1" + local channel="notifications" # default + + if echo "$raw_output" | grep -qE '^CHANNEL:[a-z][a-z0-9-]*'; then + channel=$(echo "$raw_output" | head -1 | cut -d: -f2) + raw_output=$(echo "$raw_output" | tail -n +2) + fi + + send_notification --channel "$channel" --message "$raw_output" +} +``` + +### Provider-Agnostic Adapter + +The routing layer is provider-agnostic. Plug in your platform adapter: + +``` +.squad/notify-adapter.sh # Teams / Slack / Discord / webhook -- swappable +``` + +The routing config and CHANNEL: tags never change. Only the adapter changes per deployment. + +## Anti-Patterns + +**Never send all notification types to one channel:** +``` +send_notification --channel "general" --message "$anything" +``` + +**Never use display names as identifiers (name collision risk):** +``` +send_to_team --name "Squad" --channel "notifications" +``` + +Resolve channel IDs once at setup. Use IDs at runtime. + +## Distributed Systems Pattern + +This is **pub-sub with topic routing** -- the same principle as Kafka topics, RabbitMQ routing keys, and +AWS SNS topic filtering. Route by type. Each consumer subscribes to the topics it cares about. \ No newline at end of file diff --git a/.squad/templates/skills/personal-squad/SKILL.md b/.squad/templates/skills/personal-squad/SKILL.md new file mode 100644 index 000000000..b300600d3 --- /dev/null +++ b/.squad/templates/skills/personal-squad/SKILL.md @@ -0,0 +1,65 @@ +--- +name: "personal-squad" +description: "User-level AI agents that travel with you across projects" +domain: "configuration" +confidence: "medium" +source: "manual" +--- + +# Personal Squad — Skill Document + +## What is a Personal Squad? + +A personal squad is a user-level collection of AI agents that travel with you across projects. Unlike project agents (defined in a project's `.squad/` directory), personal agents live in your global config directory and are automatically discovered when you start a squad session. + +## Directory Structure + +``` +~/.config/squad/personal-squad/ # Linux/macOS +%APPDATA%/squad/personal-squad/ # Windows +ā”œā”€ā”€ agents/ +│ ā”œā”€ā”€ {agent-name}/ +│ │ ā”œā”€ā”€ charter.md +│ │ └── history.md +│ └── ... +└── config.json # Optional: personal squad config +``` + +## How It Works + +1. **Ambient Discovery:** When Squad starts a session, it checks for a personal squad directory +2. **Merge:** Personal agents are merged into the session cast alongside project agents +3. **Ghost Protocol:** Personal agents can read project state but not write to it +4. **Kill Switch:** Set `SQUAD_NO_PERSONAL=1` to disable ambient discovery + +## Commands + +- `squad personal init` — Bootstrap a personal squad directory +- `squad personal list` — List your personal agents +- `squad personal add {name} --role {role}` — Add a personal agent +- `squad personal remove {name}` — Remove a personal agent +- `squad cast` — Show the current session cast (project + personal) + +## Ghost Protocol + +See `templates/ghost-protocol.md` for the full rules. Key points: +- Personal agents advise; project agents execute +- No writes to project `.squad/` state +- Transparent origin tagging in logs +- Project agents take precedence on conflicts + +## Configuration + +Optional `config.json` in the personal squad directory: +```json +{ + "defaultModel": "auto", + "ghostProtocol": true, + "agents": {} +} +``` + +## Environment Variables + +- `SQUAD_NO_PERSONAL` — Set to any value to disable personal squad discovery +- `SQUAD_PERSONAL_DIR` — Override the default personal squad directory path diff --git a/.squad/templates/skills/pr-review-response/SKILL.md b/.squad/templates/skills/pr-review-response/SKILL.md new file mode 100644 index 000000000..dfe290a56 --- /dev/null +++ b/.squad/templates/skills/pr-review-response/SKILL.md @@ -0,0 +1,268 @@ +--- +name: "pr-review-response" +description: "Teaches agents to reply to PR review comment threads after fixing issues, making resolutions traceable" +domain: "pull-requests, code-review, traceability" +confidence: "low" +source: "observed (agents fix review feedback silently — reviewers can't tell which comments were addressed)" +tools: + - name: "github-mcp-server-pull_request_read" + description: "Read PR review threads and comments" + when: "Step 1 — fetching review comments to understand what needs fixing" + - name: "gh api (REST)" + description: "Reply to review comment threads and resolve threads via GraphQL" + when: "Step 3 — posting reply to each comment thread after fixing" +--- + +## Context + +When an agent fixes code in response to PR review comments (from Copilot, a human reviewer, or any GitHub reviewer), the fix alone is not enough. The reviewer needs to see — on the PR thread itself — which comments were addressed and how. Without replies, comments stay visually unresolved, reviewers must re-read the entire diff to verify fixes, and there's no traceable link between feedback and resolution. + +Use this skill whenever: +- You are fixing code based on PR review feedback +- You are addressing Copilot review suggestions +- You are responding to reviewer-requested changes on a PR +- A squad member hands you review comments to resolve + +## SCOPE + +āœ… THIS SKILL PRODUCES: +- Reply comments on each review thread explaining the fix +- Optionally resolved threads (via GraphQL when appropriate) +- Commit messages that reference the PR and review context + +āŒ THIS SKILL DOES NOT PRODUCE: +- The code fixes themselves (that's the agent's domain work) +- New review comments or reviews +- PR descriptions or summaries + +## Patterns + +### Step 1: Read the review comments + +**Using MCP tools (preferred when available):** + +``` +github-mcp-server-pull_request_read + method: "get_review_comments" + owner: "{owner}" + repo: "{repo}" + pullNumber: {pr_number} +``` + +This returns review threads with metadata: `isResolved`, `isOutdated`, `isCollapsed`, and their associated comments. Each comment has an `id` you'll need for replies. + +**Using gh CLI (fallback):** + +```bash +gh api repos/{owner}/{repo}/pulls/{pr_number}/comments --paginate +``` + +Each comment object contains `id`, `body`, `path`, `line`, and `in_reply_to_id`. Top-level comments have no `in_reply_to_id` — those are the ones you reply to. + +### Step 2: Fix the code + +Make the actual code changes. This is your normal domain work — the skill doesn't prescribe how to fix, only how to communicate the fix. + +**Track what you changed.** For each review comment, note: +- The comment `id` (top-level, not a reply) +- The file and line referenced +- What you actually changed (brief description) +- The commit SHA after pushing (if available) + +### Step 3: Reply to each review thread + +After fixing and committing, reply to **each** review comment thread individually. + +**REST API call (via gh CLI):** + +```bash +gh api repos/{owner}/{repo}/pulls/{pr_number}/comments/{comment_id}/replies \ + -f body="Fixed in {sha_short} — {brief description of what was changed}" +``` + +**Important:** `{comment_id}` must be the ID of the **top-level** comment in the thread. You cannot reply to a reply — only to the original review comment. + +**Example replies:** + +```bash +# Specific and traceable +gh api repos/bradygaster/squad/pulls/42/comments/18234/replies \ + -f body="Fixed in a1b2c3d — switched to path.dirname(squadDirInfo.path) for worktree consistency" + +# When applying a suggested code change +gh api repos/bradygaster/squad/pulls/42/comments/18235/replies \ + -f body="Applied suggestion — updated error message to include the file path for debuggability" + +# When pushing back on a suggestion +gh api repos/bradygaster/squad/pulls/42/comments/18236/replies \ + -f body="Considered but not applied — this path needs to stay absolute because worktree resolution depends on it. See detectSquadDir() in detect-squad-dir.ts." +``` + +### Step 4: Resolve threads (optional, GraphQL only) + +Thread resolution is only available via the GitHub GraphQL API. Use this when your fix fully addresses the comment and no further discussion is needed. + +**First, get the thread IDs** (they're different from comment IDs): + +```bash +gh api graphql -f query=' + query { + repository(owner: "{owner}", name: "{repo}") { + pullRequest(number: {pr_number}) { + reviewThreads(first: 100) { + nodes { + id + isResolved + comments(first: 1) { + nodes { body databaseId } + } + } + } + } + } + } +' +``` + +Match thread IDs to comment IDs using `databaseId`, then resolve: + +```bash +gh api graphql -f query=' + mutation { + resolveReviewThread(input: {threadId: "{thread_node_id}"}) { + thread { id isResolved } + } + } +' +``` + +**When to resolve vs. leave open:** +- āœ… Resolve: You fixed exactly what was requested, no ambiguity +- āŒ Don't resolve: You pushed back, applied a different fix, or the comment needs further discussion +- āŒ Don't resolve: The reviewer is a human — let them confirm and resolve themselves + +**Rule of thumb:** Agent-to-agent threads (e.g., Copilot review → agent fix) can be resolved by the fixer. Human reviewer threads should be left for the human to resolve. + +### Step 5: Commit message traceability + +Commit messages should reference the PR context: + +``` +fix: address review feedback on PR #{pr_number} + +- Switched to path.dirname() for worktree path resolution (comment #18234) +- Updated error message to include file path (comment #18235) + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +``` + +For single-comment fixes, a shorter format works: + +``` +fix: use path.dirname() for worktree consistency (PR #{pr_number} review) + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +``` + +## AGENT WORKFLOW (Summary) + +1. **READ** — Fetch review threads using MCP tool or `gh api` +2. **FIX** — Make code changes, tracking comment ID → change mapping +3. **COMMIT** — Push with traceable commit message referencing PR and comments +4. **REPLY** — Post individual reply to each thread via `gh api .../replies` +5. **RESOLVE** — (Optional) Resolve agent-to-agent threads via GraphQL +6. **STOP** — Do not batch-reply, do not skip threads, do not resolve human threads + +## Examples + +### Example: Copilot flags a potential null dereference + +**Review comment (id: 55123):** +> `squadDir` could be undefined here. Consider adding a null check. + +**Agent workflow:** +1. Read the comment via `get_review_comments` +2. Add the null check in `src/cli/core/detect-squad-dir.ts` +3. Commit: `fix: add null check for squadDir (PR #99 review)` +4. Reply: + ```bash + gh api repos/bradygaster/squad/pulls/99/comments/55123/replies \ + -f body="Fixed in f4e5d6c — added early return when squadDir is undefined, matching the pattern in loadConfig()" + ``` +5. Resolve the thread (Copilot → agent, safe to resolve) + +### Example: Multiple review comments on one PR + +**Comments:** +- id: 55123 — "Null check needed" on `detect-squad-dir.ts:42` +- id: 55124 — "Consider using path.join()" on `detect-squad-dir.ts:58` +- id: 55125 — "This log message is too verbose" on `output.ts:15` + +**Agent handles each individually:** +```bash +# Fix all three, commit +git add packages/squad-cli/src/cli/core/detect-squad-dir.ts packages/squad-cli/src/cli/core/output.ts +git commit -m "fix: address 3 review comments on PR #99 + +- Added null check for squadDir (comment #55123) +- Switched to path.join() for cross-platform paths (comment #55124) +- Reduced log verbosity to debug level (comment #55125)" + +git push + +# Reply to each thread individually +gh api repos/bradygaster/squad/pulls/99/comments/55123/replies \ + -f body="Fixed — added early return when squadDir is undefined" + +gh api repos/bradygaster/squad/pulls/99/comments/55124/replies \ + -f body="Fixed — switched to path.join(squadDir, 'config.json') for cross-platform consistency" + +gh api repos/bradygaster/squad/pulls/99/comments/55125/replies \ + -f body="Fixed — changed from console.log to debug() so it only shows with --verbose flag" +``` + +### Example: Handling Copilot suggestion blocks + +Copilot sometimes provides `suggestion` blocks with exact code to apply: + +**Review comment (id: 55130):** +```` +Consider using optional chaining: +```suggestion +const name = config?.agent?.name ?? 'default'; +``` +```` + +**Reply format when applying:** +```bash +gh api repos/bradygaster/squad/pulls/99/comments/55130/replies \ + -f body="Applied suggestion — using optional chaining with nullish coalescing" +``` + +**Reply format when not applying:** +```bash +gh api repos/bradygaster/squad/pulls/99/comments/55130/replies \ + -f body="Not applied — config is guaranteed non-null at this point (validated on line 12). Optional chaining would mask errors." +``` + +### Example: Pushing back on a review comment + +Not every review comment should be accepted. When a suggestion is incorrect or doesn't apply: + +```bash +gh api repos/bradygaster/squad/pulls/99/comments/55140/replies \ + -f body="Considered but not applied — this file is in the zero-dependency bootstrap set (see copilot-instructions.md § Protected Files). Adding path.join() would require importing from the SDK, which breaks the bootstrap constraint." +``` + +Do NOT resolve the thread when pushing back. Leave it open for the reviewer to confirm. + +## Anti-Patterns + +- āŒ **Fixing silently** — Making code changes without replying to the review thread. The reviewer has no way to know which comments were addressed. +- āŒ **Batch-replying "all fixed"** — A single comment saying "Addressed all review feedback" on the PR. Each thread needs its own reply so reviewers can verify individually. +- āŒ **Resolving without explaining** — Marking threads resolved without posting a reply first. The resolution gives no context on what was done. +- āŒ **Resolving human reviewer threads** — Only resolve threads from automated reviewers (Copilot, bots). Let human reviewers confirm and resolve their own threads. +- āŒ **Vague replies** — "Fixed" or "Done" without saying what was changed. The reply should be specific enough that the reviewer doesn't need to re-read the diff. +- āŒ **Replying before pushing** — Reply after your fix is committed and pushed, not before. The reply should reference actual committed code. +- āŒ **Ignoring comments you disagree with** — If you don't apply a suggestion, reply explaining why. Silence looks like you missed it. +- āŒ **Replying to replies** — The REST API only supports replying to top-level review comments. Attempting to reply to a reply will fail with a 404. diff --git a/.squad/templates/skills/pr-screenshots/SKILL.md b/.squad/templates/skills/pr-screenshots/SKILL.md new file mode 100644 index 000000000..fc93e8f77 --- /dev/null +++ b/.squad/templates/skills/pr-screenshots/SKILL.md @@ -0,0 +1,149 @@ +--- +name: "pr-screenshots" +description: "Capture Playwright screenshots and embed them in GitHub PR descriptions" +domain: "pull-requests, visual-review, docs, testing" +confidence: "high" +source: "earned (multiple sessions establishing the pattern for PR #11 TypeDoc API reference)" +--- + +## Context + +When a PR includes visual changes (docs sites, UI components, generated pages), reviewers +need to see what the PR delivers without checking out the branch. Screenshots belong in +the **PR description body**, not as committed files and not as text descriptions. + +Use this skill whenever: +- A PR touches docs site pages (Astro, Starlight, etc.) +- A PR adds or changes UI components +- A PR generates visual artifacts (TypeDoc, Storybook, diagrams) +- Playwright tests already capture screenshots as part of testing + +## Patterns + +### 1. Capture screenshots with Playwright + +If Playwright tests already exist and produce screenshots, reuse those. Otherwise, +write a minimal capture script: + +```javascript +// scripts/capture-pr-screenshots.mjs +import { chromium } from 'playwright'; + +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); + +const screenshots = [ + { url: 'http://localhost:4321/path/to/page', name: 'feature-landing' }, + { url: 'http://localhost:4321/path/to/detail', name: 'feature-detail' }, +]; + +for (const { url, name } of screenshots) { + await page.goto(url, { waitUntil: 'networkidle' }); + await page.screenshot({ path: `screenshots/${name}.png`, fullPage: false }); +} + +await browser.close(); +``` + +### 2. Host screenshots on a temporary branch + +GitHub PR descriptions render images via URLs. The `gh` CLI cannot upload binary +images directly. Use a temporary orphan branch to host the images: + +```powershell +# Save current branch +$currentBranch = git branch --show-current + +# Create orphan branch with only screenshot files +git checkout --orphan screenshots-temp +git reset +git add screenshots/*.png +git commit -m "screenshots for PR review" +git push origin screenshots-temp --force + +# Build raw URLs +$base = "https://raw.githubusercontent.com/{owner}/{repo}/screenshots-temp/screenshots" +# Each image: $base/{name}.png + +# Return to working branch +git checkout -f $currentBranch +``` + +### 3. Embed in PR description + +Use `gh pr edit` with the raw URLs embedded as markdown images: + +```powershell +$base = "https://raw.githubusercontent.com/{owner}/{repo}/screenshots-temp/screenshots" + +gh pr edit {PR_NUMBER} --repo {owner}/{repo} --body @" +## {PR Title} + +### What this PR delivers +- {bullet points of changes} + +--- + +### Screenshots + +#### {Page/Feature Name} +![{alt text}]($base/{name}.png) + +#### {Another Page} +![{alt text}]($base/{another-name}.png) + +--- + +### To verify locally +```bash +{commands to run locally} +``` +"@ +``` + +### 4. Cleanup after merge + +After the PR is merged, delete the temporary branch: + +```bash +git push origin --delete screenshots-temp +``` + +### 5. Gitignore screenshots locally + +Screenshots are build artifacts — never commit them to feature branches: + +```gitignore +# PR screenshots (hosted on temp branch, not committed to features) +screenshots/ +docs/tests/screenshots/ +``` + +## Examples + +### Example: Docs site PR with 3 pages + +1. Start dev server: `cd docs && npm run dev` +2. Run Playwright tests (they capture screenshots as a side effect) +3. Push screenshots to `screenshots-temp` branch +4. Update PR body with embedded `![...]()` image references +5. Reviewer sees the pages inline without checking out the branch + +### Example: Reusing existing Playwright test screenshots + +If tests at `docs/tests/*.spec.mjs` already save to `docs/tests/screenshots/`: + +```powershell +cd docs && npx playwright test tests/api-reference.spec.mjs +# Screenshots now at docs/tests/screenshots/*.png +# Push those to screenshots-temp and embed in PR +``` + +## Anti-Patterns + +- āŒ **Committing screenshots to feature branches** — they bloat the repo and go stale +- āŒ **Posting text descriptions instead of actual images** — reviewers can't see what they're getting +- āŒ **Using `gh` CLI to "upload" images** — `gh issue comment` and `gh pr edit` don't support binary uploads +- āŒ **Asking the user to manually drag-drop images** — automate it with the temp branch pattern +- āŒ **Skipping screenshots for visual PRs** — if the PR changes what users see, show what users see +- āŒ **Leaving the screenshots-temp branch around forever** — clean up after merge diff --git a/.squad/templates/skills/project-conventions/SKILL.md b/.squad/templates/skills/project-conventions/SKILL.md new file mode 100644 index 000000000..48a1861da --- /dev/null +++ b/.squad/templates/skills/project-conventions/SKILL.md @@ -0,0 +1,56 @@ +--- +name: "project-conventions" +description: "Core conventions and patterns for this codebase" +domain: "project-conventions" +confidence: "medium" +source: "template" +--- + +## Context + +> **This is a starter template.** Replace the placeholder patterns below with your actual project conventions. Skills train agents on codebase-specific practices — accurate documentation here improves agent output quality. + +## Patterns + +### [Pattern Name] + +Describe a key convention or practice used in this codebase. Be specific about what to do and why. + +### Error Handling + + + + + + +### Testing + + + + + + +### Code Style + + + + + + +### File Structure + + + + + + +## Examples + +``` +// Add code examples that demonstrate your conventions +``` + +## Anti-Patterns + + +- **[Anti-pattern]** — Explanation of what not to do and why. diff --git a/.squad/templates/skills/ralph-two-pass-scan/SKILL.md b/.squad/templates/skills/ralph-two-pass-scan/SKILL.md new file mode 100644 index 000000000..dfe282a87 --- /dev/null +++ b/.squad/templates/skills/ralph-two-pass-scan/SKILL.md @@ -0,0 +1,43 @@ +--- +name: "ralph-two-pass-scan" +description: "Cuts GitHub API calls by separating lightweight list scanning from full hydration" +domain: "work-monitoring" +confidence: "high" +source: "extracted" +--- + +# Skill: Ralph — Two-Pass Issue Scanning +**Confidence:** high +**Domain:** work-monitoring +**Last validated:** 2026-03-24 + +## Context +Cuts GitHub API calls from N+1 to ~7 per round (~72% reduction) by separating list scanning from full hydration. +Addresses the scanning inefficiency described in issue #596. + +## Pattern + +### Pass 1 — Lightweight Scan + +``` +gh issue list --state open --json number,title,labels,assignees --limit 100 +``` + +**Skip hydration if ANY of these match:** + +| Condition | Skip reason | +|-----------|-------------| +| `assignees` non-empty AND no `status:needs-review` | Already owned | +| Labels contain `status:blocked` or `status:waiting-external` | Externally gated | +| Labels contain `status:done` or `status:postponed` | Closed loop | +| Title matches stale/noisy pattern (`[chore]`, `[auto]`) | Low-signal | + +### Pass 2 — Selective Hydration + +For each issue surviving Pass 1: + +``` +gh issue view --json number,title,body,labels,assignees,comments,state +``` + +Then apply normal Ralph triage logic. Rule of thumb: hydrate ≤ 30% of scanned list. If more than 30% survive Pass 1, tighten filter rules. diff --git a/.squad/templates/skills/reflect/SKILL.md b/.squad/templates/skills/reflect/SKILL.md new file mode 100644 index 000000000..6a85b5190 --- /dev/null +++ b/.squad/templates/skills/reflect/SKILL.md @@ -0,0 +1,229 @@ +--- +name: reflect +description: Learning capture system that extracts HIGH/MED/LOW confidence patterns from conversations to prevent repeating mistakes. Use after user corrections ("no", "wrong"), praise ("perfect", "exactly"), or when discovering edge cases. Complements .squad/agents/{agent}/history.md and .squad/decisions.md. +license: MIT +version: 1.0.0-squad +domain: team-memory, learning +confidence: high +--- + +# Reflect Skill + +**Critical learning capture system** for Squad. Prevents repeating mistakes and preserves successful patterns across sessions. + +Analyze conversations and propose improvements to squad knowledge based on what worked, what didn't, and edge cases discovered. **Every correction is a learning opportunity.** + +--- + +## Integration with Squad Architecture + +**Reflect complements existing Squad knowledge systems:** + +1. **`.squad/agents/{agent}/history.md`** — Permanent learnings from completed work (append-only; each agent updates their own file; Scribe propagates cross-agent updates) +2. **`.squad/decisions.md`** — Team-wide decisions that all agents respect +3. **`reflect` skill** — Captures in-flight learnings from conversations that may graduate to history.md or decisions.md + +**Workflow:** +- Use `reflect` during work to capture learnings +- At session end, review captured learnings +- Promote HIGH confidence patterns → lead agent for decision.md review +- Promote agent-specific patterns → `{agent}/history.md` updates + +--- + +## Triggers + +### šŸ”“ HIGH Priority (Invoke Immediately) + +| Trigger | Example | Why Critical | +|---------|---------|--------------| +| User correction | "no", "wrong", "not like that", "never do" | Captures mistakes to prevent repetition | +| Architectural insight | "you removed that without understanding why" | Documents design decisions (Chesterton's Fence) | +| Immediate fixes | "debug", "root cause", "fix all" | Learns from errors in real-time | + +### 🟔 MEDIUM Priority (Invoke After Multiple) + +| Trigger | Example | Why Important | +|---------|---------|---------------| +| User praise | "perfect", "exactly", "great" | Reinforces successful patterns | +| Tool preferences | "use X instead of Y", "prefer" | Builds workflow preferences | +| Edge cases | "what if X happens?", "don't forget", "ensure" | Captures scenarios to handle | + +### 🟢 LOW Priority (Invoke at Session End) + +| Trigger | Example | Why Useful | +|---------|---------|------------| +| Repeated patterns | Frequent use of specific commands/tools | Identifies workflow preferences | +| Session end | After complex work | Consolidates all session learnings | + +--- + +## Process + +### Phase 1: Identify Learning Target + +Determine what knowledge system should be updated: + +1. **Agent-specific learning** → `.squad/agents/{agent}/history.md` +2. **Team-wide decision** → `.squad/decisions/inbox/{agent}-{topic}.md` +3. **Skill-specific improvement** → Document in session, recommend to skill owner + +### Phase 2: Analyze Conversation + +Scan for learning signals with confidence levels: + +#### HIGH Confidence: Corrections + +User actively steered or corrected output. + +**Detection patterns:** +- Explicit rejection: "no", "not like that", "that's wrong" +- Strong directives: "never do", "always do", "don't ever" +- User provided alternative implementation + +**Example:** +```text +User: "No, use the azure-devops MCP tool instead of raw API calls" +→ [HIGH] + Add constraint: "Prefer azure-devops MCP tools over REST API" +``` + +#### MEDIUM Confidence: Success Patterns + +Output was accepted or praised. + +**Detection patterns:** +- Explicit praise: "perfect", "great", "yes", "exactly" +- User built on output without modification +- Output was committed without changes + +**Example:** +```text +User: "Perfect, that's exactly what I needed" +→ [MED] + Add preference: "Include usage examples in documentation" +``` + +#### MEDIUM Confidence: Edge Cases + +Scenarios not anticipated. + +**Detection patterns:** +- Questions not answered +- Workarounds user had to apply +- Error handling gaps discovered + +#### LOW Confidence: Preferences + +Accumulated patterns over time. + +--- + +### Phase 3: Propose Learnings + +Present findings: + +```text +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ REFLECTION: {target (agent/decision/skill)} │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ │ +│ [HIGH] + Add constraint: "{specific constraint}" │ +│ Source: "{quoted user correction}" │ +│ Target: .squad/decisions/inbox/{agent}-{topic}.md │ +│ │ +│ [MED] + Add preference: "{specific preference}" │ +│ Source: "{evidence from conversation}" │ +│ Target: .squad/agents/{agent}/history.md │ +│ │ +│ [LOW] ~ Note for review: "{observation}" │ +│ Source: "{pattern observed}" │ +│ Target: Session notes only │ +│ │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ Apply changes? [Y/n/edit] │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +**Confidence Threshold:** + +| Threshold | Action | +|-----------|--------| +| ≄1 HIGH signal | Always propose (user explicitly corrected) | +| ≄2 MED signals | Propose (sufficient pattern) | +| ≄3 LOW signals | Propose (accumulated evidence) | +| 1-2 LOW only | Skip (insufficient evidence) | + +### Phase 4: Persist Learnings + +**ALWAYS show changes before applying.** + +After user approval: + +1. **For Agent History:** + - Append to `.squad/agents/{agent}/history.md` under `## Learnings` section + - Format: Date, assignment context, key learning + +2. **For Team Decisions:** + - Create `.squad/decisions/inbox/{agent}-{topic}.md` + - Lead agent reviews and merges to `decisions.md` if appropriate + +3. **For Skills:** + - Document recommendation in session notes + - Squad lead reviews and routes to skill owner + +--- + +## Usage Examples + +### Example 1: User Correction + +**Conversation:** +``` +Agent: "I'll use grep to search the repository" +User: "No, use the code search tools first, grep is too slow" +``` + +**Reflection Output:** +``` +[HIGH] + Add constraint: "Use code intelligence tools before grep" + Source: "No, use the code search tools first, grep is too slow" + Target: .squad/agents/{agent}/history.md +``` + +### Example 2: Success Pattern + +**Conversation:** +``` +Agent: [Creates PR with detailed description and test plan] +User: "Perfect! This is exactly the format I want for all PRs" +``` + +**Reflection Output:** +``` +[MED] + Add preference: "Include test plan in PR descriptions" + Source: User praised detailed PR format + Target: .squad/decisions/inbox/pr-format.md (for team adoption) +``` + +--- + +## When to Use + +āœ… **Use reflect when:** +- User says "no", "wrong", "not like that" (HIGH priority) +- User says "perfect", "exactly", "great" (MED priority) +- You discover edge cases or gaps +- Complex work session with multiple learnings +- At end of sprint/milestone to consolidate patterns + +āŒ **Don't use reflect when:** +- Simple one-off questions with no pattern +- User is just exploring ideas (no concrete decisions) +- Learning is already captured in history.md/decisions.md + +--- + +## See Also + +- `.squad/decisions.md` — Team-wide decisions +- `.squad/agents/*/history.md` — Agent-specific learnings +- `.squad/routing.md` — Work assignment patterns diff --git a/.squad/templates/skills/release-process/SKILL.md b/.squad/templates/skills/release-process/SKILL.md new file mode 100644 index 000000000..c80603986 --- /dev/null +++ b/.squad/templates/skills/release-process/SKILL.md @@ -0,0 +1,217 @@ +--- +name: "release-process" +description: "Pre-release validation, npm publish procedures, and post-publish verification" +domain: "release" +confidence: "high" +source: "earned" +--- + +# Release Process + +> Earned knowledge from the v0.9.0→v0.9.1 and v0.9.4 incidents. Every agent involved in releases MUST read this before starting release work. +> See also: `.copilot/skills/release-process/SKILL.md` for the Copilot-facing runbook. + +## SCOPE + +āœ… THIS SKILL PRODUCES: +- Pre-release validation checks that prevent broken publishes +- Correct npm publish commands (never workspace-scoped) +- Fallback procedures when CI workflows fail +- Post-publish verification steps + +āŒ THIS SKILL DOES NOT PRODUCE: +- Feature implementation or test code +- Architecture decisions +- Documentation content + +## Confidence: high + +Established through the v0.9.1 incident (8-hour recovery) and reinforced by the v0.9.4 release delay (PRs #1042, #1043, #1044). Every rule below is battle-tested. + +## Context + +Squad publishes two npm packages: `@bradygaster/squad-sdk` and `@bradygaster/squad-cli`. The release pipeline flows: dev → preview → main → GitHub Release → npm publish. Brady (project owner) triggers releases — the coordinator does NOT. + +## Rules (Non-Negotiable) + +### 1. Coordinator Does NOT Publish + +The coordinator routes work and manages agents. It does NOT run `npm publish`, trigger release workflows, or make release decisions. Brady owns the release trigger. If an agent or the coordinator is asked to publish, escalate to Brady. + +### 2. Pre-Publish Dependency Validation + +Before ANY release is tagged, scan every `packages/*/package.json` for: +- `file:` references (workspace leak — the v0.9.0 root cause) +- `link:` references +- Absolute paths in dependency values +- Non-semver version strings + +**Command:** +```bash +grep -r '"file:\|"link:\|"/' packages/*/package.json +``` +If anything matches, STOP. Do not proceed. Fix the reference first. + +### 3. Never Use `npm -w` for Publishing + +`npm -w packages/squad-sdk publish` hangs silently when 2FA is enabled. Always `cd` into the package directory: + +```bash +cd packages/squad-sdk && npm publish --access public +cd packages/squad-cli && npm publish --access public +``` + +### 4. Fallback Protocol + +If `workflow_dispatch` or the publish workflow fails: +1. Try once more (ONE retry, not four) +2. If it fails again → local publish immediately +3. Do NOT attempt GitHub UI file operations to fix workflow indexing +4. GitHub has a ~15min workflow cache TTL after file renames/deletes — waiting helps, retrying doesn't + +### 5. Post-Publish Smoke Test + +After every publish, verify in a clean shell: +```bash +npm install -g @bradygaster/squad-cli@latest +squad --version # should match published version +squad doctor # should pass in a test repo +``` + +If the smoke test fails, rollback immediately. + +### 6. npm Token Must Be Automation Type + +NPM_TOKEN in CI must be an Automation token (not a user token with 2FA prompts). User tokens with `auth-and-writes` 2FA cause silent hangs in non-interactive environments. + +### 7. No Draft GitHub Releases + +Never create draft GitHub Releases. The `release: published` event only fires when a release is published — drafts don't trigger the npm publish workflow. + +### 8. Version Format + +Semantic versioning only: `MAJOR.MINOR.PATCH` (e.g., `0.9.1`). Four-part versions like `0.8.21.4` are NOT valid semver and will break npm publish. + +### 9. SKIP_BUILD_BUMP=1 in CI + +Set this environment variable in all CI build steps to prevent the build script from mutating versions during CI runs. + +## Release Checklist (Quick Reference) + +``` +ā–” All tests passing on dev +ā–” No file:/link: references in packages/*/package.json +ā–” Root package.json version matches sub-packages (v0.9.4 lesson — PR #1043) +ā–” CHANGELOG.md has ## [$VERSION] section (not just [Unreleased]) (v0.9.4 lesson — PR #1042) +ā–” Version bumps committed: npm version $VERSION --workspaces --include-workspace-root --no-git-tag-version +ā–” npm auth verified (Automation token) +ā–” No draft GitHub Releases pending +ā–” Local build + test: npm run build && npx vitest run +ā–” Push dev → CI green +ā–” Promote dev → preview (squad-promote workflow) +ā–” Preview CI green (squad-preview validates) +ā–” Promote preview → main +ā–” squad-release auto-creates GitHub Release +ā–” squad-npm-publish auto-triggers (āš ļø may be BLOCKED — see GITHUB_TOKEN limitation below) +ā–” If publish didn't trigger: gh workflow run squad-npm-publish.yml --ref main -f version=X.Y.Z +ā–” Monitor publish workflow +ā–” Post-publish smoke test +``` + +## Known Gotchas + +| Gotcha | Impact | Mitigation | +|--------|--------|------------| +| npm workspaces rewrite `"*"` → `"file:../path"` | Broken global installs | Preflight scan in CI (squad-npm-publish.yml) | +| GitHub Actions workflow cache (~15min TTL) | 422 on workflow_dispatch after file renames | Wait 15min or use local publish fallback | +| `npm -w publish` hangs with 2FA | Silent hang, no error | Never use `-w` for publish | +| Draft GitHub Releases | npm publish workflow doesn't trigger | Never create drafts | +| User npm tokens with 2FA | EOTP errors in CI | Use Automation token type | +| Root package.json version drift (v0.9.4) | squad-release.yml fails CHANGELOG check | Always bump all 3 package.json files together (PR #1043) | +| CHANGELOG.md missing `## [$VERSION]` (v0.9.4) | squad-release.yml exits with error | Convert `[Unreleased]` → `[$VERSION] - YYYY-MM-DD` before promoting to main (PR #1042) | +| GITHUB_TOKEN can't trigger downstream workflows (v0.9.4) | squad-npm-publish.yml never fires | Manual `gh workflow run` or use PAT/GitHub App token (see below) | +| Lockfile integrity check rejects workspace packages (v0.9.4) | False failures in squad-npm-publish.yml | Only validate packages resolved from npm registry (`startsWith('https://')`) (PR #1044) | +| `prebuild` version bump breaks workspace linking (v0.9.4) | Local builds fail after bump-build.mjs runs | `git checkout -- package.json packages/*/package.json` then fresh install | + +## v0.9.4 Incident Learnings + +> Source: v0.9.4 release session. PRs #1042, #1043, #1044. + +### Root Package.json Version Must Match Sub-Packages + +`squad-release.yml` reads version from ROOT `package.json` (lines 31-35): +```bash +VERSION=$(node -e "console.log(require('./package.json').version)") +if ! grep -q "## \[$VERSION\]" CHANGELOG.md; then + echo "::error::Version $VERSION not found in CHANGELOG.md" + exit 1 +fi +``` +If root package.json is behind (e.g., 0.9.1 while sub-packages are 0.9.4), the release workflow FAILS. This was the root cause of the v0.9.4 release delay — PR #1043 fixed it. + +**Rule:** When bumping versions, ALWAYS bump all 3 package.json files together: +```bash +npm version $VERSION --workspaces --include-workspace-root --no-git-tag-version +``` + +### CHANGELOG.md Must Have Version Entry + +`squad-release.yml` validates that `CHANGELOG.md` contains `## [$VERSION]`. If the version section is still `[Unreleased]` and no `[$VERSION]` section exists, the release workflow exits with error. PR #1042 fixed this for v0.9.4. + +**Rule:** Before promoting to main, convert `[Unreleased]` to `[$VERSION] - YYYY-MM-DD` in CHANGELOG.md and add a fresh `[Unreleased]` section above it. + +### GITHUB_TOKEN Event Propagation Limitation (CRITICAL) + +When `squad-release.yml` creates a GitHub Release using the default `GITHUB_TOKEN`, the `release: published` event does NOT trigger `squad-npm-publish.yml`. This is a GitHub security feature to prevent infinite workflow loops. + +**Workaround:** After the release workflow succeeds and creates the tag + GitHub Release, manually trigger the publish workflow: +```bash +gh workflow run squad-npm-publish.yml --ref main -f version=X.Y.Z +``` +IMPORTANT: Use `--ref main` to ensure the workflow runs against the main branch (where the release artifacts exist). + +**Permanent fix (TODO):** Use a PAT or GitHub App token in `squad-release.yml` instead of `GITHUB_TOKEN`. + +### Lockfile Integrity — Workspace Package Handling + +The lockfile stability check in `squad-npm-publish.yml` (line 82) filters packages for integrity hashes. Workspace packages resolve to bare relative paths (e.g., `packages/squad-sdk`), NOT `file:` URLs. The check must filter for registry-resolved packages only (`startsWith('https://')`). PR #1044 fixed this. + +### Prebuild Version Bump Breaks Local Workspace Resolution + +`scripts/bump-build.mjs` runs during `npm run prebuild` and bumps versions like `0.9.4` → `0.9.4-build.1`. This breaks workspace linking because CLI depends on exact `"@bradygaster/squad-sdk": "0.9.4"` but SDK becomes `0.9.4-build.1`. + +**Fix for local dev:** +```bash +git checkout -- package.json packages/*/package.json +rm -rf node_modules packages/*/node_modules +npm install +npm run build +``` + +### The Full Promotion Chain (v0.9.4 Documented) + +``` +dev → preview → main (via squad-promote.yml) +main push → squad-release.yml validates CHANGELOG, creates tag + GitHub Release +release published → squad-npm-publish.yml (āš ļø BLOCKED by GITHUB_TOKEN limitation) +manual workaround → gh workflow run squad-npm-publish.yml --ref main -f version=X.Y.Z +``` + +### npm Publish Workflow Dispatch Target + +When using `workflow_dispatch` to trigger `squad-npm-publish.yml`, the default ref is the repo's default branch (`dev`). Always specify `--ref main` explicitly to ensure the workflow runs against the branch with the release tag and latest workflow fixes. + +## CI Gate: Workspace Publish Policy + +The `publish-policy` job in `squad-ci.yml` scans all workflow files for bare `npm publish` commands that are missing `-w`/`--workspace` flags. Any workflow that attempts a non-workspace-scoped publish will fail CI. This prevents accidental root-level publishes that would push the wrong `package.json` to npm. + +See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation details. + +## Related + +- Issues: #556–#564 (release:next) +- v0.9.4 fixes: PR #1042 (CHANGELOG), PR #1043 (root package.json), PR #1044 (lockfile integrity) +- Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` +- CI audit: `.squad/decisions/inbox/booster-ci-audit.md` +- Copilot-level skill: `.copilot/skills/release-process/SKILL.md` +- Playbook: `PUBLISH-README.md` (repo root) diff --git a/.squad/templates/skills/reskill/SKILL.md b/.squad/templates/skills/reskill/SKILL.md new file mode 100644 index 000000000..946de0e0b --- /dev/null +++ b/.squad/templates/skills/reskill/SKILL.md @@ -0,0 +1,92 @@ +--- +name: "reskill" +description: "Team-wide charter and history optimization through skill extraction" +domain: "team-optimization" +confidence: "high" +source: "manual — Brady directive to reduce per-agent context overhead" +--- + +## Context + +When the coordinator hears "team, reskill" (or similar: "optimize context", "slim down charters"), trigger a team-wide optimization pass. The goal: reduce per-agent context consumption by extracting shared patterns from charters and histories into reusable skills. + +This is a periodic maintenance activity. Run whenever charter/history bloat is suspected. + +## Process + +### Step 1: Audit +Read all agent charters and histories. Measure byte sizes. Identify: + +- **Boilerplate** — sections repeated across ≄3 charters with <10% variation (collaboration, model, boundaries template) +- **Shared knowledge** — domain knowledge duplicated in 2+ charters (incident postmortems, technical patterns) +- **Mature learnings** — history entries appearing 3+ times across agents that should be promoted to skills + +### Step 2: Extract +For each identified pattern: +1. Create or update a skill at `.squad/skills/{skill-name}/SKILL.md` +2. Follow the skill template format (frontmatter + Context + Patterns + Examples + Anti-Patterns) +3. Set confidence: low (first observation), medium (2+ agents), high (team-wide) + +### Step 3: Trim +**Charters** — target ≤1.5KB per agent: +- Remove Collaboration section entirely (spawn prompt + agent-collaboration skill covers it) +- Remove Voice section (tagline blockquote at top of charter already captures it) +- Trim Model section to single line: `Preferred: {model}` +- Remove "When I'm unsure" boilerplate from Boundaries +- Remove domain knowledge now covered by a skill — add skill reference comment if helpful +- Keep: Identity, What I Own, unique How I Work patterns, Boundaries (domain list only) + +**Histories** — target ≤8KB per agent: +- Apply history-hygiene skill to any history >12KB +- Promote recurring patterns (3+ occurrences across agents) to skills +- Summarize old entries into `## Core Context` section +- Remove session-specific metadata (dates, branch names, requester names) + +### Step 4: Report +Output a savings table: + +| Agent | Charter Before | Charter After | History Before | History After | Saved | +|-------|---------------|---------------|----------------|---------------|-------| + +Include totals and percentage reduction. + +## Patterns + +### Minimal Charter Template (target format after reskill) + +``` +# {Name} — {Role} + +> {Tagline — one sentence capturing voice and philosophy} + +## Identity +- **Name:** {Name} +- **Role:** {Role} +- **Expertise:** {comma-separated list} + +## What I Own +- {bullet list of owned artifacts/domains} + +## How I Work +- {unique patterns and principles — NOT boilerplate} + +## Boundaries +**I handle:** {domain list} +**I don't handle:** {explicit exclusions} + +## Model +Preferred: {model} +``` + +### Skill Extraction Threshold +- **1 charter** → leave in charter (unique to that agent) +- **2 charters** → consider extracting if >500 bytes of overlap +- **3+ charters** → always extract to a shared skill + +## Anti-Patterns +- Don't delete unique per-agent identity or domain-specific knowledge +- Don't create skills for content only one agent uses +- Don't merge unrelated patterns into a single mega-skill +- Don't remove Model preference line (coordinator needs it for model selection) +- Don't touch `.squad/decisions.md` during reskill +- Don't remove the tagline blockquote — it's the charter's soul in one line diff --git a/.squad/templates/skills/retro-enforcement/SKILL.md b/.squad/templates/skills/retro-enforcement/SKILL.md new file mode 100644 index 000000000..8801e45b0 --- /dev/null +++ b/.squad/templates/skills/retro-enforcement/SKILL.md @@ -0,0 +1,148 @@ +# Skill: Retro Enforcement + +## Purpose + +Ensure retrospectives happen on schedule and that their action items are tracked in GitHub Issues — not markdown checklists. + +This skill addresses a specific, measured failure mode: **0% completion rate on markdown retro action items across 6 consecutive retrospectives**. GitHub Issues have an 85%+ completion rate in the same squad. The format was the problem, not the people. + +## Core Function: Test-RetroOverdue + +```powershell +function Test-RetroOverdue { + param( + [string]$LogDir = ".squad/log", + [int]$WindowDays = 7, + [string]$Pattern = "*retrospective*" + ) + + $cutoff = (Get-Date).AddDays(-$WindowDays) + + $retroLogs = Get-ChildItem -Path $LogDir -Filter $Pattern -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -ge $cutoff } + + return ($retroLogs.Count -eq 0) +} +``` + +### Returns +- `$true` — No retro log found within the window. **Retro is overdue. Block other work.** +- `$false` — At least one retro log found within the window. Proceed normally. + +### Detection Logic + +The function checks `.squad/log/` for any file matching `*retrospective*` dated within the last `$WindowDays` days (default: 7). If none is found, the retro is overdue. + +**File naming convention:** `.squad/log/{ISO8601-timestamp}-retrospective.md` + +Example: `.squad/log/2026-03-24T14-45-00Z-retrospective.md` + +## Coordinator Integration + +Call `Test-RetroOverdue` **at the start of every round**, before building the work queue. + +```powershell +# At round start — before any work queue construction +if (Test-RetroOverdue -LogDir ".squad/log" -WindowDays 7) { + Write-Host "[RETRO] Retrospective overdue. Running before other work." + + # Spawn retro facilitator + Invoke-RetroSession -Mode "catch-up" + + # Wait for retro log to be written + # Then resume normal round +} + +# Proceed with normal work queue +$workQueue = Get-PendingIssues | Sort-Object -Property Priority +``` + +### Blocking Semantics + +When `Test-RetroOverdue` returns `$true`: + +1. **Do not start any other work** until the retro completes +2. **Spawn the facilitator agent** (Scribe or designated) with retro mode +3. **Wait for the log file** to be written to `.squad/log/` +4. **Verify action items** were created as GitHub Issues (not markdown) +5. **Resume normal round** after retro log confirmed + +## Action Item Enforcement + +Every retro action item MUST become a GitHub Issue. The facilitator agent is responsible for this. The coordinator verifies. + +### Verification Check + +```powershell +function Test-RetroActionItemsCreated { + param([string]$RetroLogPath) + + $content = Get-Content $RetroLogPath -Raw + + # Check for Issue references (e.g., #1478, https://github.com/.../issues/1478) + $issueRefs = [regex]::Matches($content, '(?:#\d{3,}|issues/\d{3,})') + + # Check for unclosed markdown checkboxes (bad pattern) + $openCheckboxes = [regex]::Matches($content, '- \[ \]') + + if ($openCheckboxes.Count -gt 0) { + Write-Warning "[RETRO] Found $($openCheckboxes.Count) markdown checkboxes — convert to Issues" + return $false + } + + return ($issueRefs.Count -gt 0) +} +``` + +### Why Not Markdown Checklists + +From production data in tamirdresher/tamresearch1: + +| Retro | Action Items Format | Completion | +|-------|---------------------|------------| +| 2025-12-05 | Markdown `- [ ]` | 0/4 = **0%** | +| 2025-12-19 | Markdown `- [ ]` | 0/3 = **0%** | +| 2026-01-09 | Markdown `- [ ]` | 0/5 = **0%** | +| 2026-01-23 | Markdown `- [ ]` | 0/4 = **0%** | +| 2026-02-07 | Markdown `- [ ]` | 0/3 = **0%** | +| 2026-02-21 | Markdown `- [ ]` | 0/4 = **0%** | +| 2026-03-24 | GitHub Issues | 4/4 = **100%** (after enforcement) | + +**Root cause:** Markdown checklists have no assignee, no notifications, no close event, and no query surface. They are invisible to every workflow that drives completion. + +## Cadence Enforcement + +### Recommended schedule +- Weekly squads: window = 7 days +- Bi-weekly squads: window = 14 days + +### Ralph integration example + +```powershell +# ralph-watch.ps1 — round start hook +function Invoke-RoundStart { + # 1. Always check retro first + if (Test-RetroOverdue -LogDir "$RepoRoot/.squad/log" -WindowDays 7) { + Write-Host "[RALPH] Retro overdue — enforcing before work queue" + Invoke-RetroSession + return # Re-enter round after retro completes + } + + # 2. Normal work queue + $issues = Get-ReadyIssues + foreach ($issue in $issues) { + Invoke-WorkItem -Issue $issue + } +} +``` + +## Skill Metadata + +| Field | Value | +|-------|-------| +| **Skill ID** | `retro-enforcement` | +| **Category** | Ceremonies / Process | +| **Trigger** | Coordinator round start | +| **Dependencies** | `.squad/log/` directory, GitHub Issues API | +| **Tested in** | tamirdresher/tamresearch1 (production, March 2026) | +| **Outcome** | Retro cadence restored; action item completion 0% → 100% | diff --git a/.squad/templates/skills/reviewer-protocol/SKILL.md b/.squad/templates/skills/reviewer-protocol/SKILL.md new file mode 100644 index 000000000..5d589105c --- /dev/null +++ b/.squad/templates/skills/reviewer-protocol/SKILL.md @@ -0,0 +1,79 @@ +--- +name: "reviewer-protocol" +description: "Reviewer rejection workflow and strict lockout semantics" +domain: "orchestration" +confidence: "high" +source: "extracted" +--- + +## Context + +When a team member has a **Reviewer** role (e.g., Tester, Code Reviewer, Lead), they may approve or reject work from other agents. On rejection, the coordinator enforces strict lockout rules to ensure the original author does NOT self-revise. This prevents defensive feedback loops and ensures independent review. + +## Patterns + +### Reviewer Rejection Protocol + +When a team member has a **Reviewer** role: + +- Reviewers may **approve** or **reject** work from other agents. +- On **rejection**, the Reviewer may choose ONE of: + 1. **Reassign:** Require a *different* agent to do the revision (not the original author). + 2. **Escalate:** Require a *new* agent be spawned with specific expertise. +- The Coordinator MUST enforce this. If the Reviewer says "someone else should fix this," the original agent does NOT get to self-revise. +- If the Reviewer approves, work proceeds normally. + +### Strict Lockout Semantics + +When an artifact is **rejected** by a Reviewer: + +1. **The original author is locked out.** They may NOT produce the next version of that artifact. No exceptions. +2. **A different agent MUST own the revision.** The Coordinator selects the revision author based on the Reviewer's recommendation (reassign or escalate). +3. **The Coordinator enforces this mechanically.** Before spawning a revision agent, the Coordinator MUST verify that the selected agent is NOT the original author. If the Reviewer names the original author as the fix agent, the Coordinator MUST refuse and ask the Reviewer to name a different agent. +4. **The locked-out author may NOT contribute to the revision** in any form — not as a co-author, advisor, or pair. The revision must be independently produced. +5. **Lockout scope:** The lockout applies to the specific artifact that was rejected. The original author may still work on other unrelated artifacts. +6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise. +7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author. + +## Examples + +**Example 1: Reassign after rejection** +1. Fenster writes authentication module +2. Hockney (Tester) reviews → rejects: "Error handling is missing. Verbal should fix this." +3. Coordinator: Fenster is now locked out of this artifact +4. Coordinator spawns Verbal to revise the authentication module +5. Verbal produces v2 +6. Hockney reviews v2 → approves +7. Lockout clears for next artifact + +**Example 2: Escalate for expertise** +1. Edie writes TypeScript config +2. Keaton (Lead) reviews → rejects: "Need someone with deeper TS knowledge. Escalate." +3. Coordinator: Edie is now locked out +4. Coordinator spawns new agent (or existing TS expert) to revise +5. New agent produces v2 +6. Keaton reviews v2 + +**Example 3: Deadlock handling** +1. Fenster writes module → rejected +2. Verbal revises → rejected +3. Hockney revises → rejected +4. All 3 eligible agents are now locked out +5. Coordinator: "All eligible agents have been locked out. Escalating to user: [artifact details]" + +**Example 4: Reviewer accidentally names original author** +1. Fenster writes module → rejected +2. Hockney says: "Fenster should fix the error handling" +3. Coordinator: "Fenster is locked out as the original author. Please name a different agent." +4. Hockney: "Verbal, then" +5. Coordinator spawns Verbal + +## Anti-Patterns + +- āŒ Allowing the original author to self-revise after rejection +- āŒ Treating the locked-out author as an "advisor" or "co-author" on the revision +- āŒ Re-admitting a locked-out author when deadlock occurs (must escalate to user) +- āŒ Applying lockout across unrelated artifacts (scope is per-artifact) +- āŒ Accepting the Reviewer's assignment when they name the original author (must refuse and ask for a different agent) +- āŒ Clearing lockout before the revision is approved (lockout persists through revision cycle) +- āŒ Skipping verification that the revision agent is not the original author diff --git a/.squad/templates/skills/secret-handling/SKILL.md b/.squad/templates/skills/secret-handling/SKILL.md new file mode 100644 index 000000000..b0576f879 --- /dev/null +++ b/.squad/templates/skills/secret-handling/SKILL.md @@ -0,0 +1,200 @@ +--- +name: secret-handling +description: Never read .env files or write secrets to .squad/ committed files +domain: security, file-operations, team-collaboration +confidence: high +source: earned (issue #267 — credential leak incident) +--- + +## Context + +Spawned agents have read access to the entire repository, including `.env` files containing live credentials. If an agent reads secrets and writes them to `.squad/` files (decisions, logs, history), Scribe auto-commits them to git, exposing them in remote history. This skill codifies absolute prohibitions and safe alternatives. + +## Patterns + +### Prohibited File Reads + +**NEVER read these files:** +- `.env` (production secrets) +- `.env.local` (local dev secrets) +- `.env.production` (production environment) +- `.env.development` (development environment) +- `.env.staging` (staging environment) +- `.env.test` (test environment with real credentials) +- Any file matching `.env.*` UNLESS explicitly allowed (see below) + +**Allowed alternatives:** +- `.env.example` (safe — contains placeholder values, no real secrets) +- `.env.sample` (safe — documentation template) +- `.env.template` (safe — schema/structure reference) + +**If you need config info:** +1. **Ask the user directly** — "What's the database connection string?" +2. **Read `.env.example`** — shows structure without exposing secrets +3. **Read documentation** — check `README.md`, `docs/`, config guides + +**NEVER assume you can "just peek at .env to understand the schema."** Use `.env.example` or ask. + +### Prohibited Output Patterns + +**NEVER write these to `.squad/` files:** + +| Pattern Type | Examples | Regex Pattern (for scanning) | +|--------------|----------|-------------------------------| +| API Keys | `OPENAI_API_KEY=sk-proj-...`, `GITHUB_TOKEN=ghp_...` | `[A-Z_]+(?:KEY|TOKEN|SECRET)=[^\s]+` | +| Passwords | `DB_PASSWORD=super_secret_123`, `password: "..."` | `(?:PASSWORD|PASS|PWD)[:=]\s*["']?[^\s"']+` | +| Connection Strings | `postgres://user:pass@host:5432/db`, `Server=...;Password=...` | `(?:postgres|mysql|mongodb)://[^@]+@|(?:Server|Host)=.*(?:Password|Pwd)=` | +| JWT Tokens | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` | `eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+` | +| Private Keys | `-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----` | `-----BEGIN [A-Z ]+PRIVATE KEY-----` | +| AWS Credentials | `AKIA...`, `aws_secret_access_key=...` | `AKIA[0-9A-Z]{16}|aws_secret_access_key=[^\s]+` | +| Email Addresses | `user@example.com` (PII violation per team decision) | `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` | + +**What to write instead:** +- Placeholder values: `DATABASE_URL=` +- Redacted references: `API key configured (see .env.example)` +- Architecture notes: "App uses JWT auth — token stored in session" +- Schema documentation: "Requires OPENAI_API_KEY, GITHUB_TOKEN (see .env.example for format)" + +### Scribe Pre-Commit Validation + +**Before committing `.squad/` changes, Scribe MUST:** + +1. **Scan all staged files** for secret patterns (use regex table above) +2. **Check for prohibited file names** (don't commit `.env` even if manually staged) +3. **If secrets detected:** + - STOP the commit (do NOT proceed) + - Remove the file from staging: `git reset HEAD ` + - Report to user: + ``` + 🚨 SECRET DETECTED — commit blocked + + File: .squad/decisions/inbox/river-db-config.md + Pattern: DATABASE_URL=postgres://user:password@localhost:5432/prod + + This file contains credentials and MUST NOT be committed. + Please remove the secret, replace with placeholder, and try again. + ``` + - Exit with error (never silently skip) + +4. **If no secrets detected:** + - Proceed with commit as normal + +**Implementation note for Scribe:** +- Run validation AFTER staging files, BEFORE calling `git commit` +- Use PowerShell `Select-String` or `git diff --cached` to scan staged content +- Fail loud — secret leaks are unacceptable, blocking the commit is correct behavior + +### Remediation — If a Secret Was Already Committed + +**If you discover a secret in git history:** + +1. **STOP immediately** — do not make more commits +2. **Alert the user:** + ``` + 🚨 CREDENTIAL LEAK DETECTED + + A secret was found in git history: + Commit: abc1234 + File: .squad/decisions/inbox/agent-config.md + Pattern: API_KEY=sk-proj-... + + This requires immediate remediation: + 1. Revoke the exposed credential (regenerate API key, rotate password) + 2. Remove from git history (git filter-repo or BFG) + 3. Force-push the cleaned history + + Do NOT proceed with new work until this is resolved. + ``` +3. **Do NOT attempt to fix it yourself** — secret removal requires specialized tools +4. **Wait for user confirmation** before resuming work + +## Examples + +### āœ“ Correct: Reading Config Schema + +**Agent needs to know what environment variables are required:** + +``` +Agent: "What environment variables does this app need?" +→ Reads `.env.example`: + OPENAI_API_KEY=sk-... + DATABASE_URL=postgres://user:pass@localhost:5432/db + REDIS_URL=redis://localhost:6379 + +→ Writes to .squad/decisions/inbox/river-env-setup.md: + "App requires three environment variables: + - OPENAI_API_KEY (OpenAI API key, format: sk-...) + - DATABASE_URL (Postgres connection string) + - REDIS_URL (Redis connection string) + See .env.example for full schema." +``` + +### āœ— Incorrect: Reading Live Credentials + +**Agent needs to know database schema:** + +``` +Agent: (reads .env) + DATABASE_URL=postgres://admin:super_secret_pw@prod.example.com:5432/appdb + +→ Writes to .squad/decisions/inbox/river-db-schema.md: + "Database connection: postgres://admin:super_secret_pw@prod.example.com:5432/appdb" + +🚨 VIOLATION: Live credential written to committed file +``` + +**Correct approach:** +``` +Agent: (reads .env.example OR asks user) +User: "It's a Postgres database, schema is in migrations/" + +→ Writes to .squad/decisions/inbox/river-db-schema.md: + "Database: Postgres (connection configured in .env). Schema defined in db/migrations/." +``` + +### āœ“ Correct: Scribe Pre-Commit Validation + +**Scribe is about to commit:** + +```powershell +# Stage files +git add .squad/ + +# Scan staged content for secrets +$stagedContent = git diff --cached +$secretPatterns = @( + '[A-Z_]+(?:KEY|TOKEN|SECRET)=[^\s]+', + '(?:PASSWORD|PASS|PWD)[:=]\s*["'']?[^\s"'']+', + 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' +) + +$detected = $false +foreach ($pattern in $secretPatterns) { + if ($stagedContent -match $pattern) { + $detected = $true + Write-Host "🚨 SECRET DETECTED: $($matches[0])" + break + } +} + +if ($detected) { + # Remove from staging, report, exit + git reset HEAD .squad/ + Write-Error "Commit blocked — secret detected in staged files" + exit 1 +} + +# Safe to commit +git commit -F $msgFile +``` + +## Anti-Patterns + +- āŒ Reading `.env` "just to check the schema" — use `.env.example` instead +- āŒ Writing "sanitized" connection strings that still contain credentials +- āŒ Assuming "it's just a dev environment" makes secrets safe to commit +- āŒ Committing first, scanning later — validation MUST happen before commit +- āŒ Silently skipping secret detection — fail loud, never silent +- āŒ Trusting agents to "know better" — enforce at multiple layers (prompt, hook, architecture) +- āŒ Writing secrets to "temporary" files in `.squad/` — Scribe commits ALL `.squad/` changes +- āŒ Extracting "just the host" from a connection string — still leaks infrastructure topology diff --git a/.squad/templates/skills/session-recovery/SKILL.md b/.squad/templates/skills/session-recovery/SKILL.md new file mode 100644 index 000000000..05cfbae60 --- /dev/null +++ b/.squad/templates/skills/session-recovery/SKILL.md @@ -0,0 +1,155 @@ +--- +name: "session-recovery" +description: "Find and resume interrupted Copilot CLI sessions using session_store queries" +domain: "workflow-recovery" +confidence: "high" +source: "earned" +tools: + - name: "sql" + description: "Query session_store database for past session history" + when: "Always — session_store is the source of truth for session history" +--- + +## Context + +Squad agents run in Copilot CLI sessions that can be interrupted — terminal crashes, network drops, machine restarts, or accidental window closes. When this happens, in-progress work may be left in a partially-completed state: branches with uncommitted changes, issues marked in-progress with no active agent, or checkpoints that were never finalized. + +Copilot CLI stores session history in a SQLite database called `session_store` (read-only, accessed via the `sql` tool with `database: "session_store"`). This skill teaches agents how to query that store to detect interrupted sessions and resume work. + +## Patterns + +### 1. Find Recent Sessions + +Query the `sessions` table filtered by time window. Include the last checkpoint to understand where the session stopped: + +```sql +SELECT + s.id, + s.summary, + s.cwd, + s.branch, + s.updated_at, + (SELECT title FROM checkpoints + WHERE session_id = s.id + ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint +FROM sessions s +WHERE s.updated_at >= datetime('now', '-24 hours') +ORDER BY s.updated_at DESC; +``` + +### 2. Filter Out Automated Sessions + +Automated agents (monitors, keep-alive, heartbeat) create high-volume sessions that obscure human-initiated work. Exclude them: + +```sql +SELECT s.id, s.summary, s.cwd, s.updated_at, + (SELECT title FROM checkpoints + WHERE session_id = s.id + ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint +FROM sessions s +WHERE s.updated_at >= datetime('now', '-24 hours') + AND s.id NOT IN ( + SELECT DISTINCT t.session_id FROM turns t + WHERE t.turn_index = 0 + AND (LOWER(t.user_message) LIKE '%keep-alive%' + OR LOWER(t.user_message) LIKE '%heartbeat%') + ) +ORDER BY s.updated_at DESC; +``` + +### 3. Search by Topic (FTS5) + +Use the `search_index` FTS5 table for keyword search. Expand queries with synonyms since this is keyword-based, not semantic: + +```sql +SELECT DISTINCT s.id, s.summary, s.cwd, s.updated_at +FROM search_index si +JOIN sessions s ON si.session_id = s.id +WHERE search_index MATCH 'auth OR login OR token OR JWT' + AND s.updated_at >= datetime('now', '-48 hours') +ORDER BY s.updated_at DESC +LIMIT 10; +``` + +### 4. Search by Working Directory + +```sql +SELECT s.id, s.summary, s.updated_at, + (SELECT title FROM checkpoints + WHERE session_id = s.id + ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint +FROM sessions s +WHERE s.cwd LIKE '%my-project%' + AND s.updated_at >= datetime('now', '-48 hours') +ORDER BY s.updated_at DESC; +``` + +### 5. Get Full Session Context Before Resuming + +Before resuming, inspect what the session was doing: + +```sql +-- Conversation turns +SELECT turn_index, substr(user_message, 1, 200) AS ask, timestamp +FROM turns WHERE session_id = 'SESSION_ID' ORDER BY turn_index; + +-- Checkpoint progress +SELECT checkpoint_number, title, overview +FROM checkpoints WHERE session_id = 'SESSION_ID' ORDER BY checkpoint_number; + +-- Files touched +SELECT file_path, tool_name +FROM session_files WHERE session_id = 'SESSION_ID'; + +-- Linked PRs/issues/commits +SELECT ref_type, ref_value +FROM session_refs WHERE session_id = 'SESSION_ID'; +``` + +### 6. Detect Orphaned Issue Work + +Find sessions that were working on issues but may not have completed: + +```sql +SELECT DISTINCT s.id, s.branch, s.summary, s.updated_at, + sr.ref_type, sr.ref_value +FROM sessions s +JOIN session_refs sr ON s.id = sr.session_id +WHERE sr.ref_type = 'issue' + AND s.updated_at >= datetime('now', '-48 hours') +ORDER BY s.updated_at DESC; +``` + +Cross-reference with `gh issue list --label "status:in-progress"` to find issues that are marked in-progress but have no active session. + +### 7. Resume a Session + +Once you have the session ID: + +```bash +# Resume directly +copilot --resume SESSION_ID +``` + +## Examples + +**Recovering from a crash during PR creation:** +1. Query recent sessions filtered by branch name +2. Find the session that was working on the PR +3. Check its last checkpoint — was the code committed? Was the PR created? +4. Resume or manually complete the remaining steps + +**Finding yesterday's work on a feature:** +1. Use FTS5 search with feature keywords +2. Filter to the relevant working directory +3. Review checkpoint progress to see how far the session got +4. Resume if work remains, or start fresh with the context + +## Anti-Patterns + +- āŒ Searching by partial session IDs — always use full UUIDs +- āŒ Resuming sessions that completed successfully — they have no pending work +- āŒ Using `MATCH` with special characters without escaping — wrap paths in double quotes +- āŒ Skipping the automated-session filter — high-volume automated sessions will flood results +- āŒ Assuming FTS5 is semantic search — it's keyword-based; always expand queries with synonyms +- āŒ Ignoring checkpoint data — checkpoints show exactly where the session stopped diff --git a/.squad/templates/skills/squad-conventions/SKILL.md b/.squad/templates/skills/squad-conventions/SKILL.md new file mode 100644 index 000000000..72eca68ed --- /dev/null +++ b/.squad/templates/skills/squad-conventions/SKILL.md @@ -0,0 +1,69 @@ +--- +name: "squad-conventions" +description: "Core conventions and patterns used in the Squad codebase" +domain: "project-conventions" +confidence: "high" +source: "manual" +--- + +## Context +These conventions apply to all work on the Squad CLI tool (`create-squad`). Squad is a zero-dependency Node.js package that adds AI agent teams to any project. Understanding these patterns is essential before modifying any Squad source code. + +## Patterns + +### Zero Dependencies +Squad has zero runtime dependencies. Everything uses Node.js built-ins (`fs`, `path`, `os`, `child_process`). Do not add packages to `dependencies` in `package.json`. This is a hard constraint, not a preference. + +### Node.js Built-in Test Runner +Tests use `node:test` and `node:assert/strict` — no test frameworks. Run with `npm test`. Test files live in `test/`. The test command is `node --test test/`. + +### Error Handling — `fatal()` Pattern +All user-facing errors use the `fatal(msg)` function which prints a red `āœ—` prefix and exits with code 1. Never throw unhandled exceptions or print raw stack traces. The global `uncaughtException` handler calls `fatal()` as a safety net. + +### ANSI Color Constants +Colors are defined as constants at the top of `index.js`: `GREEN`, `RED`, `DIM`, `BOLD`, `RESET`. Use these constants — do not inline ANSI escape codes. + +### File Structure +- `.squad/` — Team state (user-owned, never overwritten by upgrades) +- `.squad/templates/` — Template files copied from `templates/` (Squad-owned, overwritten on upgrade) +- `.github/agents/squad.agent.md` — Coordinator prompt (Squad-owned, overwritten on upgrade) +- `templates/` — Source templates shipped with the npm package +- `.squad/skills/` — Team skills in SKILL.md format (user-owned) +- `.squad/decisions/inbox/` — Drop-box for parallel decision writes + +### Windows Compatibility +Always use `path.join()` for file paths — never hardcode `/` or `\` separators. Squad must work on Windows, macOS, and Linux. All tests must pass on all platforms. + +### Init Idempotency +The init flow uses a skip-if-exists pattern: if a file or directory already exists, skip it and report "already exists." Never overwrite user state during init. The upgrade flow overwrites only Squad-owned files. + +### Copy Pattern +`copyRecursive(src, target)` handles both files and directories. It creates parent directories with `{ recursive: true }` and uses `fs.copyFileSync` for files. + +## Examples + +```javascript +// Error handling +function fatal(msg) { + console.error(`${RED}āœ—${RESET} ${msg}`); + process.exit(1); +} + +// File path construction (Windows-safe) +const agentDest = path.join(dest, '.github', 'agents', 'squad.agent.md'); + +// Skip-if-exists pattern +if (!fs.existsSync(ceremoniesDest)) { + fs.copyFileSync(ceremoniesSrc, ceremoniesDest); + console.log(`${GREEN}āœ“${RESET} .squad/ceremonies.md`); +} else { + console.log(`${DIM}ceremonies.md already exists — skipping${RESET}`); +} +``` + +## Anti-Patterns +- **Adding npm dependencies** — Squad is zero-dep. Use Node.js built-ins only. +- **Hardcoded path separators** — Never use `/` or `\` directly. Always `path.join()`. +- **Overwriting user state on init** — Init skips existing files. Only upgrade overwrites Squad-owned files. +- **Raw stack traces** — All errors go through `fatal()`. Users see clean messages, not stack traces. +- **Inline ANSI codes** — Use the color constants (`GREEN`, `RED`, `DIM`, `BOLD`, `RESET`). diff --git a/.squad/templates/skills/test-discipline/SKILL.md b/.squad/templates/skills/test-discipline/SKILL.md new file mode 100644 index 000000000..d222bed52 --- /dev/null +++ b/.squad/templates/skills/test-discipline/SKILL.md @@ -0,0 +1,37 @@ +--- +name: "test-discipline" +description: "Update tests when changing APIs — no exceptions" +domain: "quality" +confidence: "high" +source: "earned (Fenster/Hockney incident, test assertion sync violations)" +--- + +## Context + +When APIs or public interfaces change, tests must be updated in the same commit. When test assertions reference file counts or expected arrays, they must be kept in sync with disk reality. Stale tests block CI for other contributors. + +## Patterns + +- **API changes → test updates (same commit):** If you change a function signature, public interface, or exported API, update the corresponding tests before committing +- **Test assertions → disk reality:** When test files contain expected counts (e.g., `EXPECTED_FEATURES`, `EXPECTED_SCENARIOS`), they must match the actual files on disk +- **Add files → update assertions:** When adding docs pages, features, or any counted resource, update the test assertion array in the same commit +- **CI failures → check assertions first:** Before debugging complex failures, verify test assertion arrays match filesystem state + +## Examples + +āœ“ **Correct:** +- Changed auth API signature → updated auth.test.ts in same commit +- Added `distributed-mesh.md` to features/ → added `'distributed-mesh'` to EXPECTED_FEATURES array +- Deleted two scenario files → removed entries from EXPECTED_SCENARIOS + +āœ— **Incorrect:** +- Changed spawn parameters → committed without updating casting.test.ts (CI breaks for next person) +- Added `built-in-roles.md` → left EXPECTED_FEATURES at old count (PR blocked) +- Test says "expected 7 files" but disk has 25 (assertion staleness) + +## Anti-Patterns + +- Committing API changes without test updates ("I'll fix tests later") +- Treating test assertion arrays as static (they evolve with content) +- Assuming CI passing means coverage is correct (stale assertions can pass while being wrong) +- Leaving gaps for other agents to discover diff --git a/.squad/templates/skills/tiered-memory/SKILL.md b/.squad/templates/skills/tiered-memory/SKILL.md new file mode 100644 index 000000000..bb82e662c --- /dev/null +++ b/.squad/templates/skills/tiered-memory/SKILL.md @@ -0,0 +1,234 @@ +--- +name: tiered-memory +description: Three-tier agent memory model (hot/cold/wiki) for 20-55% context reduction per spawn +domain: memory-management, performance +confidence: high +source: earned (production measurements in tamirdresher/tamresearch1, 34-74KB baseline payloads) +--- + +# Skill: Tiered Agent Memory + +## Overview + +Squad agents currently load their full context history on every spawn, resulting in 34–74KB payloads per agent (8,800–18,500 tokens). Measurement shows 82–96% of that context is "old noise" — information that is no longer relevant to the current task. The Tiered Agent Memory skill introduces a three-tier memory model that eliminates this bloat, achieving 20–55% context reduction per spawn in production. + +--- + +## Memory Tiers + +### šŸ”„ Hot Tier — Current Session Context +- **Size target:** ~2–4KB +- **Load policy:** Always loaded. Every spawn includes hot memory by default. +- **Contents:** Current task description, active decisions made this session, immediate blockers, last 3–5 actions taken, who you are talking to right now. +- **Lifetime:** Current session only. Discarded after session ends (Scribe promotes relevant parts to Cold). +- **Purpose:** Provide immediate task context without any latency or load decision. + +### ā„ļø Cold Tier — Summarized Cross-Session History +- **Size target:** ~8–12KB +- **Load policy:** Load on demand. Include only when the task explicitly needs history. +- **Contents:** Summarized past sessions (compressed by Scribe), cross-session decisions, recurring patterns, unresolved issues from prior work. +- **Lifetime:** 30 days rolling window. After 30 days, Scribe promotes to Wiki tier. +- **Purpose:** Answer "what have we tried before?" and "what was decided?" without replaying full transcripts. +- **How to include:** Pass `--include-cold` in spawn template or add `## Cold Memory` section. + +### šŸ“š Wiki Tier — Durable Structured Knowledge +- **Size target:** variable, structured reference docs +- **Load policy:** Async write, selective read. Load only when task requires domain knowledge. +- **Contents:** Architecture decisions (ADRs), agent charters, routing rules, stable conventions, external API contracts, known platform constraints. +- **Lifetime:** Permanent until explicitly deprecated. +- **Purpose:** Authoritative reference. Not history — structured facts. +- **How to include:** Pass `--include-wiki` or reference specific wiki doc paths in spawn template. + +--- + +## When to Load Each Tier + +| Situation | Hot | Cold | Wiki | +|-----------|-----|------|------| +| New task, no prior context needed | āœ… | āŒ | āŒ | +| Resuming interrupted work | āœ… | āœ… | āŒ | +| Debugging a recurring issue | āœ… | āœ… | āŒ | +| Implementing against a spec/ADR | āœ… | āŒ | āœ… | +| Onboarding to unfamiliar subsystem | āœ… | āŒ | āœ… | +| Post-incident review | āœ… | āœ… | āœ… | + +--- + +## Spawn Template Pattern + +The default spawn prompt should include **Hot tier only**: + +``` +## Memory Context + +### Hot (current session) +{hot_context} +``` + +Add `--include-cold` when the task needs history: +``` +## Memory Context + +### Hot (current session) +{hot_context} + +### Cold (summarized history — load on demand) +See: .squad/memory/cold/{agent-name}.md +``` + +Add `--include-wiki` when the task needs domain knowledge: +``` +## Memory Context + +### Hot (current session) +{hot_context} + +### Wiki (durable reference) +See: .squad/memory/wiki/{topic}.md +``` + +--- + +## Measurement Data + +Baseline measurements from tamirdresher/tamresearch1 production runs (June 2025): + +| Agent | Total Context | Old Noise % | Hot-Only Size | Savings | +|-------|--------------|-------------|---------------|---------| +| Picard (Lead) | 74KB / 18.5K tokens | 96% | ~3KB | 55% | +| Scribe | 52KB / 13K tokens | 91% | ~4KB | 48% | +| Data | 43KB / 10.7K tokens | 88% | ~3.5KB | 42% | +| Ralph | 38KB / 9.5K tokens | 85% | ~3KB | 38% | +| Worf | 34KB / 8.5K tokens | 82% | ~3KB | 20% | + +**Average savings: 20–55% per spawn** with Hot-only loading. Cold + Wiki on-demand adds ~2–8KB when needed, still well below current baselines. + +--- + +## Integration with Scribe Agent + +Scribe is the memory coordinator for this system. It automates tier promotion: + +1. **End of session:** Scribe compresses Hot → Cold summary (keeps ~10% of session verbosity) +2. **After 30 days:** Scribe promotes Cold → Wiki for decisions/facts that aged into stable knowledge +3. **On-demand wiki writes:** Any agent can request Scribe to write a wiki entry mid-session using `scribe:wiki-write` + +See Scribe charter: `.squad/agents/scribe/charter.md` + +--- + +## Implementation Checklist + +- [ ] Scribe writes Hot context file at session start (`.squad/memory/hot/{agent}.md`) +- [ ] Scribe compresses and writes Cold summary at session end +- [ ] Spawn templates default to Hot-only +- [ ] Coordinators add `--include-cold` / `--include-wiki` flags as needed +- [ ] Wiki entries stored in `.squad/memory/wiki/` +- [ ] Cold entries stored in `.squad/memory/cold/` with 30-day TTL + +--- + +## References + +- Upstream issue: bradygaster/squad#600 +- Production data: tamirdresher/tamresearch1 (June 2025) + +--- + +## Spawn Template + +# Spawn Template: Agent with Tiered Memory + +Use this template when spawning any Squad agent. By default it loads **Hot tier only**. Add optional sections as needed. + +--- + +## Task + +{task_description} + +## WHY + +{why_this_matters} + +## Success Criteria + +- [ ] {criterion_1} +- [ ] {criterion_2} + +--- + +## Memory Context + +### šŸ”„ Hot (always included) + +> Paste current session context here (2–4KB max): + +``` +Current task: {task_description} +Active decisions: {decisions_this_session} +Last actions: {last_3_to_5_actions} +Blockers: {current_blockers_or_none} +Talking to: {current_interlocutor} +``` + +--- + +### ā„ļø Cold (include when task needs history — add `--include-cold`) + +> Load on demand. Do not inline unless specifically needed. + +Summarized cross-session history is at: +`.squad/memory/cold/{agent-name}.md` + +Include when: +- Resuming interrupted work +- Debugging a recurring issue +- "What have we tried before?" + +**To load cold memory, add this section and fetch the file before spawning:** + +``` +## Cold Memory Summary +{contents_of_.squad/memory/cold/{agent-name}.md} +``` + +--- + +### šŸ“š Wiki (include when task needs domain knowledge — add `--include-wiki`) + +> Load on demand. Reference specific wiki docs by path. + +Wiki entries are at: `.squad/memory/wiki/` + +Include when: +- Implementing against an ADR or spec +- Onboarding to unfamiliar subsystem +- Need stable conventions or API contracts + +**To load wiki, add this section and reference the specific doc:** + +``` +## Wiki Reference +{contents_of_.squad/memory/wiki/{topic}.md} +``` + +--- + +## Escalation + +If blocked or uncertain: +- Architecture questions → @picard +- Security concerns → @worf +- Infrastructure/deployment → @belanna +- Memory/history questions → @scribe + +--- + +## Notes + +- Hot tier is always included and should stay under 4KB +- Cold adds ~8–12KB; only include when history is relevant +- Wiki adds variable size; only include specific relevant docs +- See `skills/tiered-memory/SKILL.md` for full tier reference +- See `docs/tiered-memory-guide.md` for wiring instructions diff --git a/.squad/templates/skills/versioning-policy/SKILL.md b/.squad/templates/skills/versioning-policy/SKILL.md new file mode 100644 index 000000000..997d4c4aa --- /dev/null +++ b/.squad/templates/skills/versioning-policy/SKILL.md @@ -0,0 +1,119 @@ +--- +name: "versioning-policy" +description: "Semver versioning rules for Squad SDK and CLI — prevents prerelease version incidents" +domain: "release, versioning, npm, CI" +confidence: "medium" +source: "earned (PR #640 workspace resolution incident, PR #116 prerelease leak, CI gate implementation)" +--- + +## Context + +Squad is a monorepo with two publishable npm packages (`@bradygaster/squad-sdk` and `@bradygaster/squad-cli`) managed via npm workspaces. Version mismatches and prerelease leaks have caused production incidents — most notably PR #640, where a `-build.N` prerelease version silently broke workspace dependency resolution. + +This skill codifies the versioning rules every agent must follow. + +## 1. Version Format + +All packages use **strict semver**: `MAJOR.MINOR.PATCH` + +- āœ… `0.9.1`, `1.0.0`, `0.10.0` +- āŒ `0.9.1-build.4`, `0.9.1-preview.1`, `0.8.6.1-preview` + +No prerelease suffixes on `dev` or `main` branches — ever. + +## 2. Prerelease Versions Are Ephemeral + +The `scripts/bump-build.mjs` script creates `-build.N` versions (e.g., `0.9.1-build.4`) for **local development testing only**. + +Rules: +- `-build.N` versions are created automatically during local `npm run build` +- They are **never committed** to `dev` or `main` +- The script skips itself in CI (`CI=true` or `SKIP_BUILD_BUMP=1`) +- If you see a `-build.N` version in a PR diff, it is a bug — reject the PR + +## 3. SDK and CLI Version Sync + +Both `@bradygaster/squad-sdk` and `@bradygaster/squad-cli` **MUST have the same version** at all times. The root `package.json` version must also match. + +`bump-build.mjs` enforces this by updating all three `package.json` files in lockstep (root + `packages/squad-sdk` + `packages/squad-cli`). + +If versions diverge, workspace resolution silently breaks (see §4). + +## 4. npm Workspace Semver Footgun + +The CLI depends on the SDK via a workspace dependency with a semver range: + +```json +"@bradygaster/squad-sdk": ">=0.9.0" +``` + +**Critical:** Per the semver specification, `>=0.9.0` does **NOT** match `0.9.1-build.4`. + +Semver prerelease versions (anything with a `-` suffix) are only matched by ranges that explicitly reference the same `MAJOR.MINOR.PATCH` base with a prerelease comparator. A bare `>=0.9.0` range skips all prerelease versions. + +**What happens:** When the local SDK has version `0.9.1-build.4`, npm's workspace resolution fails to match the `>=0.9.0` range. npm then **silently installs a stale published version** from the npm registry instead of using the local workspace link. The build succeeds but runs against old SDK code. + +This is the root cause of the **PR #640 incident**, where workspace packages appeared linked but were actually running against stale registry versions. + +## 5. Who Bumps Versions + +**Surgeon (Release Manager) owns all version bumps.** + +| Agent | May modify `version` in package.json? | +|-------|---------------------------------------| +| Surgeon | āœ… Yes — sole owner of version bumps | +| Any other agent | āŒ No — unless explicitly fixing a prerelease leak | + +If you discover a prerelease version committed to `dev` or `main`, you may fix it (revert to the clean release version) without Surgeon's approval. This is a safety escape hatch, not a license to manage versions. + +## 6. Version Bump Lifecycle + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Development phase │ +│ Versions stay at current release: 0.9.1 │ +│ bump-build.mjs creates -build.N locally (not committed)│ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ Pre-release testing │ +│ bump-build.mjs → 0.9.1-build.1, -build.2, ... │ +│ Local only. Never committed. Never pushed. │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ Release │ +│ Surgeon bumps to next version (e.g., 0.9.2 or 0.10.0) │ +│ Tags, publishes to npm registry │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ Post-release │ +│ Versions stay at the new release version (e.g., 0.9.2) │ +│ Development continues on clean version │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## 7. CI Enforcement + +The **`prerelease-version-guard`** CI gate blocks any PR to `dev` or `main` that contains prerelease version strings in `package.json` files. + +- The gate scans all three `package.json` files for `-` in the version field +- PRs with prerelease versions **cannot merge** until the version is cleaned +- The `skip-version-check` label bypasses the gate — use **only** for the bump-build script's own PR (if applicable), and only with Surgeon's approval + +## 8. Incident Reference — PR #640 + +**PR #640** is the cautionary tale for this entire policy. + +**What happened:** Prerelease versions (`0.9.1-build.4`) were committed to a branch. The workspace dependency `>=0.9.0` failed to match the prerelease version per semver spec. npm silently installed a stale published SDK from the registry instead of linking the local workspace copy. Four PRs (#637–#640) attempted iterative patches before the root cause was identified. + +**Root cause:** No versioning policy existed. Agents didn't know that prerelease versions break workspace resolution, or that only Surgeon should modify versions. + +**Resolution:** This skill, the `prerelease-version-guard` CI gate, and the team decision to centralize version ownership under Surgeon. + +## Quick Reference + +| Rule | Summary | +|------|---------| +| Format | `MAJOR.MINOR.PATCH` — no prerelease on dev/main | +| Prerelease | `-build.N` is local-only, never committed | +| Sync | SDK + CLI + root must have identical versions | +| Ownership | Surgeon bumps versions; others don't touch them | +| CI gate | `prerelease-version-guard` blocks prerelease PRs | +| Escape hatch | Any agent may revert a prerelease leak to clean version | +| Footgun | `>=0.9.0` does NOT match `0.9.1-build.4` per semver | diff --git a/.squad/templates/skills/windows-compatibility/SKILL.md b/.squad/templates/skills/windows-compatibility/SKILL.md new file mode 100644 index 000000000..6242b88c4 --- /dev/null +++ b/.squad/templates/skills/windows-compatibility/SKILL.md @@ -0,0 +1,98 @@ +--- +name: "windows-compatibility" +description: "Cross-platform path handling and command patterns" +domain: "platform" +confidence: "high" +source: "earned (multiple Windows-specific bugs: colons in filenames, git -C failures, path separators)" +--- + +## Context + +Squad runs on Windows, macOS, and Linux. Several bugs have been traced to platform-specific assumptions: ISO timestamps with colons (illegal on Windows), `git -C` with Windows paths (unreliable), forward-slash paths in Node.js on Windows. + +## Patterns + +### Filenames & Timestamps +- **Never use colons in filenames:** ISO 8601 format `2026-03-15T05:30:00Z` is illegal on Windows +- **Use `safeTimestamp()` utility:** Replaces colons with hyphens → `2026-03-15T05-30-00Z` +- **Centralize formatting:** Don't inline `.toISOString().replace(/:/g, '-')` — use the utility + +### Git Commands +- **Never use `git -C {path}`:** Unreliable with Windows paths (backslashes, spaces, drive letters) +- **Always `cd` first:** Change directory, then run git commands +- **Check for changes before commit:** `git diff --cached --quiet` (exit 0 = no changes) + +### Commit Messages +- **Never embed newlines in `-m` flag:** Backtick-n (`\n`) fails silently in PowerShell +- **Use temp file + `-F` flag:** Write message to file, commit with `git commit -F $msgFile` + +### Paths +- **Never assume CWD is repo root:** Always use `TEAM ROOT` from spawn prompt or run `git rev-parse --show-toplevel` +- **Use path.join() or path.resolve():** Don't manually concatenate with `/` or `\` + +### Path Comparison (Case Sensitivity) +- **Never use case-sensitive `startsWith` or `===` for path comparison on Windows or macOS:** These filesystems are case-insensitive — `C:\Users\` and `c:\users\` refer to the same location +- **Use platform-aware comparison:** Check `process.platform === 'win32' || process.platform === 'darwin'` and lowercase both sides before comparing +- **Pattern:** + ```typescript + const CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'darwin'; + + function pathStartsWith(fullPath: string, prefix: string): boolean { + if (CASE_INSENSITIVE) { + return fullPath.toLowerCase().startsWith(prefix.toLowerCase()); + } + return fullPath.startsWith(prefix); + } + ``` +- **Where it matters:** Security checks (path traversal prevention), rootDir confinement, any path-contains-path validation +- **Linux is case-sensitive:** Do NOT lowercase on Linux — `/Home/` and `/home/` are different directories + +## Examples + +āœ“ **Correct:** +```javascript +// Timestamp utility +const safeTimestamp = () => new Date().toISOString().replace(/:/g, '-').split('.')[0] + 'Z'; + +// Git workflow (PowerShell) +cd $teamRoot +git add .squad/ +if ($LASTEXITCODE -eq 0) { + $msg = @" +docs(ai-team): session log + +Changes: +- Added decisions +"@ + $msgFile = [System.IO.Path]::GetTempFileName() + Set-Content -Path $msgFile -Value $msg -Encoding utf8 + git commit -F $msgFile + Remove-Item $msgFile +} +``` + +āœ— **Incorrect:** +```javascript +// Colon in filename +const logPath = `.squad/log/${new Date().toISOString()}.md`; // ILLEGAL on Windows + +// git -C with Windows path +exec('git -C C:\\src\\squad add .squad/'); // UNRELIABLE + +// Inline newlines in commit message +exec('git commit -m "First line\nSecond line"'); // FAILS silently in PowerShell +``` + +## Anti-Patterns + +- Testing only on one platform (bugs ship to other platforms) +- Assuming Unix-style paths work everywhere +- Using `git -C` because it "looks cleaner" (it doesn't work) +- Skipping `git diff --cached --quiet` check (creates empty commits) +- **Wrong — case-sensitive path check on Windows and macOS:** + ```typescript + if (!resolved.startsWith(rootDir + path.sep)) { + throw new Error('Path traversal blocked'); + } + // Fails: 'c:\\Users\\temp\\file'.startsWith('C:\\Users\\temp\\') → false + ``` diff --git a/.squad/templates/spawn-reference.md b/.squad/templates/spawn-reference.md new file mode 100644 index 000000000..b0ee47899 --- /dev/null +++ b/.squad/templates/spawn-reference.md @@ -0,0 +1,127 @@ +# Spawn Reference + +### How to Spawn an Agent + +**You MUST dispatch every agent spawn** via the platform's tool (`task` on CLI, `runSubagent` on VS Code): + +- **`agent_type`**: `"general-purpose"` (always — this gives agents full tool access) +- **`mode`**: `"background"` (default) or `"sync"` — use `"background"` for all parallelizable work; use `"sync"` only when the result is needed before the next step can proceed +- **`description`**: `"{Name}: {brief task summary}"` (e.g., `"Ripley: Design REST API endpoints"`, `"Dallas: Build login form"`) — this is what appears in the UI, so it MUST carry the agent's name and what they're doing +- **`prompt`**: The full agent prompt (see below) + +**⚔ Inline the charter.** Before spawning, read the agent's `charter.md` (resolve from team root: `{team_root}/.squad/agents/{name}/charter.md`) and paste its contents directly into the spawn prompt. This eliminates a tool call from the agent's critical path. The agent still reads its own `history.md` and `decisions.md`. + +**Background spawn (the default):** Use the template below with `mode: "background"`. + +**Sync spawn (when required):** Use the template below and omit the `mode` parameter (sync is default). + +> **VS Code equivalent:** Use `runSubagent` with the prompt content below. Drop `agent_type`, `mode`, `model`, and `description` parameters. Multiple subagents in one turn run concurrently. Sync is the default on VS Code. + +**Template for any agent** (substitute `{Name}`, `{Role}`, `{name}`, and inline the charter): + +``` +agent_type: "general-purpose" +model: "{resolved_model}" +mode: "background" +name: "{name}" +description: "{emoji} {Name}: {brief task summary}" +prompt: | + You are {Name}, the {Role} on this project. + + YOUR CHARTER: + {paste contents of .squad/agents/{name}/charter.md here} + + TEAM ROOT: {team_root} + CURRENT_DATETIME: + All `.squad/` paths are relative to this root. + + PERSONAL_AGENT: {true|false} # Whether this is a personal agent + GHOST_PROTOCOL: {true|false} # Whether ghost protocol applies + + {If PERSONAL_AGENT is true, append Ghost Protocol rules:} + ## Ghost Protocol + You are a personal agent operating in a project context. You MUST follow these rules: + - Read-only project state: Do NOT write to project's .squad/ directory + - No project ownership: You advise; project agents execute + - Transparent origin: Tag all logs with [personal:{name}] + - Consult mode: Provide recommendations, not direct changes + {end Ghost Protocol block} + + WORKTREE_PATH: {worktree_path} + WORKTREE_MODE: {true|false} + + {% if WORKTREE_MODE %} + **WORKTREE:** You are working in a dedicated worktree at `{WORKTREE_PATH}`. + - All file operations should be relative to this path + - Do NOT switch branches — the worktree IS your branch (`{branch_name}`) + - Build and test in the worktree, not the main repo + - Commit and push from the worktree + {% endif %} + + STATE_BACKEND: {state_backend} + + ## State Protocol — Runtime State Tools + Mutable squad state is owned by the runtime. You MUST use the `state.*` tools + whenever they are available: + - `squad_state_read` / `squad_state_list` for decisions, history, logs, and inbox entries + - `squad_state_write` / `squad_state_append` for durable updates + - `squad_state_delete` after Scribe merges inbox entries + - `squad_state_health` when diagnosing backend availability + - `squad_decide` for team-relevant decisions + + The runtime routes those calls to the configured backend (`{state_backend}`), including + git-native backends. Do NOT run backend git commands, switch to a state branch, push + note refs, or write mutable `.squad/` state files by hand. Static config (charters, + team.md, routing.md, skills) remains on disk and may be read with normal file tools. + + Read `agents/{name}/history.md` with `squad_state_read` when state tools are available; otherwise fall back to `.squad/agents/{name}/history.md`. + Read `decisions.md` with `squad_state_read` when state tools are available; otherwise fall back to `.squad/decisions.md`. + If .squad/identity/wisdom.md exists, read it before starting work. + If .squad/identity/now.md exists, read it at spawn time. + Check .copilot/skills/ for copilot-level skills (process, workflow, protocol). + Check .squad/skills/ for team-level skills (patterns discovered during work). + Read any relevant SKILL.md files before working. + + āš ļø WORK FRESHNESS: When determining what to work on: + - If an external tracker is configured (GitHub Issues, GitLab Issues, Azure DevOps), + ALWAYS query it for current open/active items. The tracker is the authoritative + source of truth — local plan files and checkboxes are advisory only. + - If .squad/identity/now.md has a `last_verified` timestamp older than your session + start, re-verify the current focus against the tracker before acting. + - NEVER work on items marked closed/done in the tracker, even if local files + suggest they are incomplete. + + {only if MCP tools detected — omit entirely if none:} + MCP TOOLS: {service}: āœ… ({tools}) | āŒ. Fall back to CLI when unavailable. + {end MCP block} + + **Requested by:** {current user name} + + INPUT ARTIFACTS: {list exact file paths to review/modify} + + The user says: "{message}" + + Do the work. Respond as {Name}. + + āš ļø OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL. + āš ļø DATES: When writing dates in any file (decisions, history, logs), use ONLY the CURRENT_DATETIME value above. Never infer or guess the date. + + AFTER work (BEST-EFFORT — do NOT retry on failure): + āš ļø POST-WORK BUDGET: Spend at most 20 tool calls on post-work steps below. + If you are running low on context or have used 60+ tool calls on primary work, + skip post-work entirely -- Scribe handles it independently. + 1. APPEND learnings with `squad_state_append` to `agents/{name}/history.md`. + Include architecture decisions, patterns, user preferences, and key file paths. + 2. If you made a team-relevant decision, call `squad_decide`. If that tool is + unavailable, use `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. + 3. If state tools are unavailable, skip post-work state persistence and report the + backend/tool availability problem in your final summary. + 4. SKILL EXTRACTION is handled by Scribe — do NOT attempt it yourself. + + āš ļø STOP ON FAILURE: If ANY post-work step fails (git conflict, file not found, + permission error), SKIP it and move on. Do NOT retry. Scribe handles cleanup + independently. Your primary deliverable is already done — post-work is optional. + + āš ļø RESPONSE ORDER: After ALL tool calls, write a 2-3 sentence plain text + summary as your FINAL output. No tool calls after this summary. +``` diff --git a/.squad/templates/squad.agent.md.template b/.squad/templates/squad.agent.md.template new file mode 100644 index 000000000..2deb49af3 --- /dev/null +++ b/.squad/templates/squad.agent.md.template @@ -0,0 +1,910 @@ +--- +name: Squad +description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." +--- + + + +You are **Squad (Coordinator)** — the orchestrator for this project's AI team. + +### Coordinator Identity + +- **Name:** Squad (Coordinator) +- **Version:** 0.0.0-source (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v{version}` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Role:** Agent orchestration, handoff enforcement, reviewer gating +- **Inputs:** User request, repository state, `.squad/decisions.md` +- **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) +- **Mindset:** **"What can I launch RIGHT NOW?"** — always maximize parallel work +- **Refusal rules:** + - You may NOT generate domain artifacts (code, designs, analyses) — spawn an agent + - You may NOT bypass reviewer approval on rejected work + - You may NOT invent facts or assumptions — ask the user or spawn an agent who knows + - You may NOT do work yourself — ALWAYS delegate to a team member, even for small tasks. The only exception is Direct Mode (status checks, factual questions, and simple answers from context — see Response Mode Selection). + +### State & Team Root Resolution (before mode check) + +Before deciding Init vs Team mode, resolve where the team state actually lives: + +1. **Read `.squad/config.json`** (if it exists in the current `.squad/` directory). +2. **External state** — if `stateLocation` is `"external"`: + - Resolve the external state path: `{platform_appdata}/squad/projects/{projectKey}/` + - The team root is that external path. Load `team.md` from there. +3. **Remote/satellite mode** — if `teamRoot` is present: + - The team root is the value of `teamRoot` (absolute path to another `.squad/` directory). + - Load `team.md` from `{teamRoot}/.squad/team.md` (or `{teamRoot}/team.md` if teamRoot already points inside `.squad/`). +4. **Neither** — team root is the local `.squad/` directory (default behavior). + +Store the resolved team root as `TEAM_ROOT`. All subsequent `.squad/` path references use this root. + +### Mode-Switch Check + +Check: Does `{TEAM_ROOT}/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs) +- **No** → Init Mode +- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast) +- **Yes, with roster entries** → Team Mode + +--- + +## Init Mode — Phase 1: Propose the Team + +No team exists yet. Propose one — but **DO NOT create any files until the user confirms.** + +1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey {user}, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** +2. Ask: *"What are you building? (language, stack, what it does)"* +3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): + - Determine team size (typically 4–5 + Scribe). + - Determine assignment shape from the user's project description. + - Derive resonance signals from the session and repo context. + - Select a universe. Allocate character names from that universe. + - Scribe is always "Scribe" — exempt from casting. + - Ralph is always "Ralph" — exempt from casting. +4. Propose the team with their cast names. Example (names will vary per cast): + +``` +šŸ—ļø {CastName1} — Lead Scope, decisions, code review +āš›ļø {CastName2} — Frontend Dev React, UI, components +šŸ”§ {CastName3} — Backend Dev APIs, database, services +🧪 {CastName4} — Tester Tests, quality, edge cases +šŸ“‹ Scribe — (silent) Memory, decisions, session logs +šŸ”„ Ralph — (monitor) Work queue, backlog, keep-alive +``` + +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: + - **question:** *"Look right?"* + - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` + +**āš ļø STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** + +--- + +## Init Mode — Phase 2: Create the Team + +**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). + +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. + +6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). + +**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). + +**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. + +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. + +**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: +``` +.squad/decisions.md merge=union +.squad/agents/*/history.md merge=union +.squad/log/** merge=union +.squad/orchestration-log/** merge=union +``` +The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically. + +7. Say: *"āœ… Team hired. Try: '{FirstCastName}, set up the project structure'"* + +8. **Post-setup input sources** (optional — ask after team is created, not during casting): + - PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow + - GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow + - Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section + - Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment + - These are additive. Don't block — if the user skips or gives a task instead, proceed immediately. + +--- + +## Team Mode + +**āš ļø CRITICAL RULE: You are a DISPATCHER, not a DOER. Every task that needs domain expertise MUST be dispatched to a specialist agent — never performed inline.** + +**DISPATCH MECHANISM (detect once per session, then use consistently):** +- **CLI:** `task` tool → use it with agent_type, mode, model, name, description, prompt +- **VS Code:** `runSubagent` tool → use it with the full agent prompt +- **Neither available:** work inline (fallback only — LAST RESORT) + +**If you wrote code, generated artifacts, or produced domain work without dispatching to an agent, you violated this rule. The coordinator ROUTES — it does not BUILD. No exceptions.** + +**On every session start:** Run `git config user.name` to identify the current user, and **resolve the team root** (see Worktree Awareness). Store the team root — all `.squad/` paths must be resolved relative to it. Resolve `CURRENT_DATETIME` once from the `` value in your system context. Sanity-check that it is a real ISO-like timestamp, not placeholder text, with a plausible year and timezone (`Z` or an offset). If the system value is missing or implausible, run a local date command and use that result instead (`date +"%Y-%m-%dT%H:%M:%S%z"` on macOS/Linux, or `Get-Date -Format o` in PowerShell). Pass the team root and the resolved literal current datetime into every spawn prompt as `TEAM_ROOT` and `CURRENT_DATETIME` respectively. Never pass placeholder text for `CURRENT_DATETIME`. Pass the current user's name into every agent spawn prompt and Scribe log so the team always knows who requested the work. Check `.squad/identity/now.md` if it exists — it tells you what the team was last focused on. Update it if the focus has shifted. + +**Resolve state backend:** Read `.squad/config.json` (at the resolved TEAM_ROOT) and check the `stateBackend` field. Valid values: `"worktree"` (default), `"git-notes"`, `"orphan"`, `"two-layer"`. Store as `STATE_BACKEND` and pass it into every spawn prompt. This determines how agents read and write mutable state (history, decisions, logs). Static config (charters, team.md, routing.md) always lives on disk regardless of backend. The `"two-layer"` option combines git-notes (commit-scoped annotations) with orphan branch (permanent state) — see the blog post for the full architecture. + +**⚔ Context caching:** After the first message in a session, `team.md`, `routing.md`, and `registry.json` are already in your context. Do NOT re-read them on subsequent messages — you already have the roster, routing rules, and cast names. Only re-read if the user explicitly modifies the team (adds/removes members, changes routing). + +**Session catch-up (lazy — not on every start):** Do NOT scan logs on every session start. Only provide a catch-up summary when: +- The user explicitly asks ("what happened?", "catch me up", "status", "what did the team do?") +- The coordinator detects a different user than the one in the most recent session log + +When triggered: +1. Scan `.squad/orchestration-log/` for entries newer than the last session log in `.squad/log/`. +2. Present a brief summary: who worked, what they did, key decisions made. +3. Keep it to 2-3 sentences. The user can dig into logs and decisions if they want the full picture. + +**Casting migration check:** If `.squad/team.md` exists but `.squad/casting/` does not, perform the migration described in "Casting & Persistent Naming → Migration — Already-Squadified Repos" before proceeding. + +### Personal Squad (Ambient Discovery) + +Before assembling the session cast, check for personal agents: + +1. **Kill switch check:** If `SQUAD_NO_PERSONAL` is set, skip personal agent discovery entirely. +2. **Resolve personal dir:** Call `resolvePersonalSquadDir()` — returns the user's personal squad path or null. +3. **Discover personal agents:** If personal dir exists, scan `{personalDir}/agents/` for charter.md files. +4. **Merge into cast:** Personal agents are additive — they don't replace project agents. On name conflict, project agent wins. +5. **Apply Ghost Protocol:** All personal agents operate under Ghost Protocol (read-only project state, no direct file edits, transparent origin tagging). + +**Spawn personal agents with:** +- Charter from personal dir (not project) +- Ghost Protocol rules appended to system prompt +- `origin: 'personal'` tag in all log entries +- Consult mode: personal agents advise, project agents execute + +### Issue Awareness + +**On every session start (after resolving team root):** Check for open GitHub issues assigned to squad members via labels. Use the GitHub CLI or API to list issues with `squad:*` labels: + +``` +gh issue list --label "squad:{member-name}" --state open --json number,title,labels,body --limit 10 +``` + +For each squad member with assigned issues, note them in the session context. When presenting a catch-up or when the user asks for status, include pending issues: + +``` +šŸ“‹ Open issues assigned to squad members: + šŸ”§ {Backend} — #42: Fix auth endpoint timeout (squad:ripley) + āš›ļø {Frontend} — #38: Add dark mode toggle (squad:dallas) +``` + +**Proactive issue pickup:** If a user starts a session and there are open `squad:{member}` issues, mention them: *"Hey {user}, {AgentName} has an open issue — #42: Fix auth endpoint timeout. Want them to pick it up?"* + +**Issue triage routing:** When a new issue gets the `squad` label (via the sync-squad-labels workflow), the Lead triages it — reading the issue, analyzing it, assigning the correct `squad:{member}` label(s), and commenting with triage notes. The Lead can also reassign by swapping labels. + +**⚔ Read `.squad/team.md` (roster), `.squad/routing.md` (routing), and `.squad/casting/registry.json` (persistent names) as parallel tool calls in a single turn. Do NOT read these sequentially.** + +### Acknowledge Immediately — "Feels Heard" + +**The user should never see a blank screen while agents work.** Before spawning any background agents, ALWAYS respond with brief text acknowledging the request. Name the agents being launched and describe their work in human terms — not system jargon. This acknowledgment is REQUIRED, not optional. + +- **Single agent:** `"Fenster's on it — looking at the error handling now."` +- **Multi-agent spawn:** Show a quick launch table: + ``` + šŸ”§ Fenster — error handling in index.js + 🧪 Hockney — writing test cases + šŸ“‹ Scribe — logging session + ``` + +The acknowledgment goes in the same response as the `task` tool calls — text first, then tool calls. Keep it to 1-2 sentences plus the table. Don't narrate the plan; just show who's working on what. + +### Role Emoji in Task Descriptions + +When spawning agents, include the role emoji in the `description` parameter to make task lists visually scannable. The emoji should match the agent's role from `team.md`. + +**Standard role emoji mapping:** + +| Role Pattern | Emoji | Examples | +|--------------|-------|----------| +| Lead, Architect, Tech Lead | šŸ—ļø | "Lead", "Senior Architect", "Technical Lead" | +| Frontend, UI, Design | āš›ļø | "Frontend Dev", "UI Engineer", "Designer" | +| Backend, API, Server | šŸ”§ | "Backend Dev", "API Engineer", "Server Dev" | +| Test, QA, Quality | 🧪 | "Tester", "QA Engineer", "Quality Assurance" | +| DevOps, Infra, Platform | āš™ļø | "DevOps", "Infrastructure", "Platform Engineer" | +| Docs, DevRel, Technical Writer | šŸ“ | "DevRel", "Technical Writer", "Documentation" | +| Data, Database, Analytics | šŸ“Š | "Data Engineer", "Database Admin", "Analytics" | +| Security, Auth, Compliance | šŸ”’ | "Security Engineer", "Auth Specialist" | +| Scribe | šŸ“‹ | "Session Logger" (always Scribe) | +| Ralph | šŸ”„ | "Work Monitor" (always Ralph) | +| @copilot | šŸ¤– | "Coding Agent" (GitHub Copilot) | + +**How to determine emoji:** +1. Look up the agent in `team.md` (already cached after first message) +2. Match the role string against the patterns above (case-insensitive, partial match) +3. Use the first matching emoji +4. If no match, use šŸ‘¤ as fallback + +**Examples:** +- `name: "keaton"`, `description: "šŸ—ļø Keaton: Reviewing architecture proposal"` +- `name: "fenster"`, `description: "šŸ”§ Fenster: Refactoring auth module"` +- `name: "hockney"`, `description: "🧪 Hockney: Writing test cases"` +- `name: "scribe"`, `description: "šŸ“‹ Scribe: Log session & merge decisions"` + +The `name` parameter generates the human-readable agent ID shown in the tasks panel — it MUST be the agent's lowercase cast name (e.g., `"eecom"`, `"fido"`). Without it, the platform shows generic slugs like "general-purpose-task" instead of the cast name. The emoji in `description` makes task spawn notifications visually consistent with the launch table shown to users. + +### Directive Capture + +**Before routing any message, check: is this a directive?** A directive is a user statement that sets a preference, rule, or constraint the team should remember. Capture it to the decisions inbox BEFORE routing work. + +**Directive signals** (capture these): +- "Always…", "Never…", "From now on…", "We don't…", "Going forward…" +- Naming conventions, coding style preferences, process rules +- Scope decisions ("we're not doing X", "keep it simple") +- Tool/library preferences ("use Y instead of Z") + +**NOT directives** (route normally): +- Work requests ("build X", "fix Y", "test Z", "add a feature") +- Questions ("how does X work?", "what did the team do?") +- Agent-directed tasks ("Ripley, refactor the API") + +**When you detect a directive:** + +1. Capture the directive with the runtime state tools when available: + - Prefer `squad_state_write` to write `decisions/inbox/copilot-directive-{timestamp}.md` using this format: + ``` + ### {timestamp}: User directive + **By:** {user name} (via Copilot) + **What:** {the directive, verbatim or lightly paraphrased} + **Why:** User request — captured for team memory + ``` + - Do **not** run `git notes`, checkout `squad-state`, or manually commit mutable `.squad/` state. The runtime owns state persistence. +2. Acknowledge briefly: `"šŸ“Œ Captured. {one-line summary of the directive}."` +3. If the message ALSO contains a work request, route that work normally after capturing. If it's directive-only, you're done — no agent spawn needed. + +### Memory Governance Tools + +When memory tools are available, use them before writing durable memory by hand: + +- Classify candidate memories with `memory.classify`. +- Persist approved durable facts, decisions, and policies with `memory.write`. +- Search governed memory with `memory.search` before relying only on raw file search. +- Promote, delete, and audit governed entries with `memory.promote`, `memory.delete`, and `memory.audit`. + +If memory tools are not available, use runtime state tools for durable Squad state when present. In MCP sessions these are exposed as `squad_state_read`, `squad_state_write`, `squad_state_append`, `squad_state_delete`, `squad_state_list`, and `squad_state_health` aliases. Only fall back to local `.squad/` file writes when `STATE_BACKEND` is `worktree`/`local` and no runtime state tool exists. For `git-notes`, `orphan`, or `two-layer`, do not hand-write mutable state; report that the `squad_state` MCP/runtime state bridge is missing. Never claim provider-backed Copilot Memory, semantic indexing, or remote deletion unless a configured tool or CLI bridge performed the operation. External semantic memory is opt-in; forbidden or transient content must not be persisted. + +### Routing + +The routing table determines **WHO** handles work. After routing, use Response Mode Selection to determine **HOW** (Direct/Lightweight/Standard/Full). + +| Signal | Action | +|--------|--------| +| Names someone ("Ripley, fix the button") | Spawn that agent | +| Personal agent by name (user addresses a personal agent) | Route to personal agent in consult mode — they advise, project agent executes changes | +| "Team" or multi-domain question | Spawn 2-3+ relevant agents in parallel, synthesize | +| Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | +| Issue suitable for @copilot (when @copilot is on the roster) | Check capability profile in team.md, suggest routing to @copilot if it's a good fit | +| Ceremony request ("design meeting", "run a retro") | Run the matching ceremony from `ceremonies.md` (see Ceremonies) | +| Issues/backlog request ("pull issues", "show backlog", "work on #N") | Follow GitHub Issues Mode (see that section) | +| PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | +| Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | +| Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | +| General work request | Check routing.md, spawn best match + any anticipatory agents | +| Quick factual question | Answer directly (no spawn) | +| Ambiguous | Pick the most likely agent; say who you chose | +| Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | + +**Skill-aware routing:** Before spawning, check BOTH skill directories for skills relevant to the task domain: +1. `.copilot/skills/` — **Copilot-level skills.** Foundational process knowledge (release process, git workflow, reviewer protocol, etc.). These are the coordinator's own playbook — check first. +2. `.squad/skills/` — **Team-level skills.** Patterns and practices agents discovered during work. + +If a matching skill exists, add to the spawn prompt: `Relevant skill: {path}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. + +### Consult Mode Detection + +When a user addresses a personal agent by name: +1. Route the request to the personal agent +2. Tag the interaction as consult mode +3. If the personal agent recommends changes, hand off execution to the appropriate project agent +4. Log: `[consult] {personal-agent} → {project-agent}: {handoff summary}` + +### Skill Confidence Lifecycle + +Skills use a three-level confidence model. Confidence only goes up, never down. + +| Level | Meaning | When | +|-------|---------|------| +| `low` | First observation | Agent noticed a reusable pattern worth capturing | +| `medium` | Confirmed | Multiple agents or sessions independently observed the same pattern | +| `high` | Established | Consistently applied, well-tested, team-agreed | + +Confidence bumps when an agent independently validates an existing skill — applies it in their work and finds it correct. If an agent reads a skill, uses the pattern, and it works, that's a confirmation worth bumping. + +### Response Mode Selection + +After routing determines WHO handles work, select the response MODE based on task complexity. Bias toward upgrading — when uncertain, go one tier higher rather than risk under-serving. + +| Mode | When | How | Target | +|------|------|-----|--------| +| **Direct** | Status checks, factual questions the coordinator already knows, simple answers from context | Coordinator answers directly — NO agent spawn | ~2-3s | +| **Lightweight** | Single-file edits, small fixes, follow-ups, simple scoped read-only queries | Spawn ONE agent with minimal prompt (see Lightweight Spawn Template). Use `agent_type: "explore"` for read-only queries | ~8-12s | +| **Standard** | Normal tasks, single-agent work requiring full context | Spawn one agent with full ceremony — charter inline, history read, decisions read. This is the current default | ~25-35s | +| **Full** | Multi-agent work, complex tasks touching 3+ concerns, "Team" requests | Parallel fan-out, full ceremony, Scribe included | ~40-60s | + +**Direct Mode exemplars** (coordinator answers instantly, no spawn): +- "Where are we?" → Summarize current state from context: branch, recent work, what the team's been doing. A user favorite — make it instant. +- "How many tests do we have?" → Run a quick command, answer directly. +- "What branch are we on?" → `git branch --show-current`, answer directly. +- "Who's on the team?" → Answer from team.md already in context. +- "What did we decide about X?" → Answer from decisions.md already in context. + +**Lightweight Mode exemplars** (one agent, minimal prompt): +- "Fix the typo in README" → Spawn one agent, no charter, no history read. +- "Add a comment to line 42" → Small scoped edit, minimal context needed. +- "What does this function do?" → `agent_type: "explore"` (Haiku model, fast). +- Follow-up edits after a Standard/Full response — context is fresh, skip ceremony. + +**Standard Mode exemplars** (one agent, full ceremony): +- "{AgentName}, add error handling to the export function" +- "{AgentName}, review the prompt structure" +- Any task requiring architectural judgment or multi-file awareness. + +**Full Mode exemplars** (multi-agent, parallel fan-out): +- "Team, build the login page" +- "Add OAuth support" +- Any request that touches 3+ agent domains. + +**Mode upgrade rules:** +- If a Lightweight task turns out to need history or decisions context → treat as Standard. +- If uncertain between Direct and Lightweight → choose Lightweight. +- If uncertain between Lightweight and Standard → choose Standard. +- Never downgrade mid-task. If you started Standard, finish Standard. + +**Lightweight Spawn Template** (skip charter, history, and decisions reads — just the task): + +``` +agent_type: "general-purpose" +model: "{resolved_model}" +mode: "background" +name: "{name}" +description: "{emoji} {Name}: {brief task summary}" +prompt: | + You are {Name}, the {Role} on this project. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + WORKTREE_PATH: {worktree_path} + WORKTREE_MODE: {true|false} + **Requested by:** {current user name} + + {% if WORKTREE_MODE %} + **WORKTREE:** Working in `{WORKTREE_PATH}`. All operations relative to this path. Do NOT switch branches. + {% endif %} + + TASK: {specific task description} + TARGET FILE(S): {exact file path(s)} + + Do the work. Keep it focused. + If you made a meaningful decision, persist it with `squad_decide` when available, or `squad_state_write` to `decisions/inbox/{name}-{brief-slug}.md`. Do not run git notes, switch branches, or write mutable `.squad/` state by hand. + + āš ļø OUTPUT: Report outcomes in human terms. Never expose tool internals or SQL. + āš ļø RESPONSE ORDER: After ALL tool calls, write a plain text summary as FINAL output. +``` + +For read-only queries, use the explore agent: `agent_type: "explore"` with `"You are {Name}, the {Role}. CURRENT_DATETIME: — {question} TEAM ROOT: {team_root}"` + +### Per-Agent Model Selection + +Resolve a model before every spawn. Honor persistent config first, then session directives, charter preferences, and task-aware auto-selection; keep the cost-first rule unless code or prompt architecture is being written. + +Use silent fallback chains when a chosen model is unavailable, and omit the `model` parameter for platform default or nuclear fallback. + +**On-demand reference:** Read `.squad/templates/model-selection-reference.md` for the full layer hierarchy, role mapping, fallback chains, spawn formatting, and valid models catalog. + +### Client Compatibility + +Detect the client surface once per session and adapt spawning behavior accordingly: CLI uses `task`/`read_agent`, VS Code uses `runSubagent`, and inline work is last-resort fallback only. + +Do not rely on CLI-only capabilities such as per-spawn model control or the `sql` tool in cross-platform paths. + +**On-demand reference:** Read `.squad/templates/client-compatibility-reference.md` for platform detection, VS Code adaptations, feature degradation, and SQL caveats. + +### MCP Integration + +MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. + +> **Config details:** Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. + +#### Detection + +At task start, scan your available tools list for known MCP prefixes: +- `github-mcp-server-*` → GitHub API (issues, PRs, code search, actions) +- `trello_*` → Trello boards, cards, lists +- `aspire_*` → Aspire dashboard (metrics, logs, health) +- `azure_*` → Azure resource management +- `notion_*` → Notion pages and databases + +If tools with these prefixes exist, they are available. If not, fall back to CLI equivalents or inform the user. + +#### Passing MCP Context to Spawned Agents + +When spawning agents, include an `MCP TOOLS AVAILABLE` block in the prompt (see spawn template below). This tells agents what's available without requiring them to discover tools themselves. Only include this block when MCP tools are actually detected — omit it entirely when none are present. + +#### Routing MCP-Dependent Tasks + +- **Coordinator handles directly** when the MCP operation is simple (a single read, a status check) and doesn't need domain expertise. +- **Spawn with context** when the task needs agent expertise AND MCP tools. Include the MCP block in the spawn prompt so the agent knows what's available. +- **Explore agents never get MCP** — they have read-only local file access. Route MCP work to `general-purpose` or `task` agents, or handle it in the coordinator. + +#### Graceful Degradation + +Never crash or halt because an MCP tool is missing. MCP tools are enhancements, not dependencies. + +1. **CLI fallback** — GitHub MCP missing → use `gh` CLI. Azure MCP missing → use `az` CLI. +2. **Inform the user** — "Trello integration requires the Trello MCP server. Add it to `.copilot/mcp-config.json`." +3. **Continue without** — Log what would have been done, proceed with available tools. + +### Eager Execution Philosophy + +> **āš ļø Exception:** Eager Execution does NOT apply during Init Mode Phase 1. Init Mode requires explicit user confirmation (via `ask_user`) before creating the team. Do NOT launch file creation, directory scaffolding, or any Phase 2 work until the user confirms the roster. + +The Coordinator's default mindset is **launch aggressively, collect results later.** + +- When a task arrives, don't just identify the primary agent — identify ALL agents who could usefully start work right now, **including anticipatory downstream work**. +- A tester can write test cases from requirements while the implementer builds. A docs agent can draft API docs while the endpoint is being coded. Launch them all. +- After agents complete, immediately ask: *"Does this result unblock more work?"* If yes, launch follow-up agents without waiting for the user to ask. +- Agents should note proactive work clearly: `šŸ“Œ Proactive: I wrote these test cases based on the requirements while {BackendAgent} was building the API. They may need adjustment once the implementation is final.` + +### Mode Selection — Background is the Default + +Before spawning, assess: **is there a reason this MUST be sync?** If not, use background. + +**Use `mode: "sync"` ONLY when:** + +| Condition | Why sync is required | +|-----------|---------------------| +| Agent B literally cannot start without Agent A's output file | Hard data dependency | +| A reviewer verdict gates whether work proceeds or gets rejected | Approval gate | +| The user explicitly asked a question and is waiting for a direct answer | Direct interaction | +| The task requires back-and-forth clarification with the user | Interactive | + +**Everything else is `mode: "background"`:** + +| Condition | Why background works | +|-----------|---------------------| +| Scribe (always) | Never needs input, never blocks | +| Any task with known inputs | Start early, collect when needed | +| Writing tests from specs/requirements/demo scripts | Inputs exist, tests are new files | +| Scaffolding, boilerplate, docs generation | Read-only inputs | +| Multiple agents working the same broad request | Fan-out parallelism | +| Anticipatory work — tasks agents know will be needed next | Get ahead of the queue | +| **Uncertain which mode to use** | **Default to background** — cheap to collect later | + +### Parallel Fan-Out + +When the user gives any task, the Coordinator MUST: + +1. **Decompose broadly.** Identify ALL agents who could usefully start work, including anticipatory work (tests, docs, scaffolding) that will obviously be needed. +2. **Check for hard data dependencies only.** Shared memory files (decisions, logs) use the drop-box pattern and are NEVER a reason to serialize. The only real conflict is: "Agent B needs to read a file that Agent A hasn't created yet." +3. **Spawn all independent agents as `mode: "background"` in a single tool-calling turn.** Multiple `task` calls in one response is what enables true parallelism. +4. **Show the user the full launch immediately:** + ``` + šŸ—ļø {Lead} analyzing project structure... + āš›ļø {Frontend} building login form components... + šŸ”§ {Backend} setting up auth API endpoints... + 🧪 {Tester} writing test cases from requirements... + ``` +5. **Chain follow-ups.** When background agents complete, immediately assess: does this unblock more work? Launch it without waiting for the user to ask. + +**Example — "Team, build the login page":** +- Turn 1: Spawn {Lead} (architecture), {Frontend} (UI), {Backend} (API), {Tester} (test cases from spec) — ALL background, ALL in one tool call +- Collect results. Scribe merges decisions. +- Turn 2: If {Tester}'s tests reveal edge cases, spawn {Backend} (background) for API edge cases. If {Frontend} needs design tokens, spawn a designer (background). Keep the pipeline moving. + +**Example — "Add OAuth support":** +- Turn 1: Spawn {Lead} (sync — architecture decision needing user approval). Simultaneously spawn {Tester} (background — write OAuth test scenarios from known OAuth flows without waiting for implementation). +- After {Lead} finishes and user approves: Spawn {Backend} (background, implement) + {Frontend} (background, OAuth UI) simultaneously. + +### Shared File Architecture — Drop-Box Pattern + +To enable full parallelism, shared writes use a drop-box pattern that eliminates file conflicts: + +**decisions.md** — Agents do NOT write directly to `decisions.md`. Instead: +- Agents record decisions with `squad_decide` or `squad_state_write` to `decisions/inbox/{agent-name}-{brief-slug}.md`. +- The runtime routes that write to the configured state backend. Agents must not run `git notes`, switch to `squad-state`, or hand-roll backend commits. +- Scribe merges into the canonical `.squad/decisions.md` and clears the inbox +- All agents READ from `.squad/decisions.md` at spawn time (last-merged snapshot) + +**orchestration-log/** — Scribe writes one entry per agent after each batch: +- `.squad/orchestration-log/{timestamp}-{agent-name}.md` +- The coordinator passes a spawn manifest to Scribe; Scribe creates the files +- Format matches the existing orchestration log entry template +- Append-only, never edited after write + +**history.md** — No change. Each agent writes only to its own `history.md` (already conflict-free). + +**log/** — No change. Already per-session files. + +### Worktree Awareness + +Resolve `TEAM_ROOT` before routing work. All `.squad/` paths are relative to that root, and every spawned agent must receive the resolved `TEAM_ROOT` value rather than discovering it independently. + +Use worktree-local state by default for concurrent work; allow explicit overrides when the user wants main-checkout or externalized state. + +**On-demand reference:** Read `.squad/templates/worktree-reference.md` for team-root resolution, worktree strategies, lifecycle rules, and pre-spawn setup. + +### Worktree Lifecycle Management + +When worktree mode is enabled, issue-based work should get a dedicated worktree and branch without disrupting the main checkout. Reuse existing issue worktrees when present and clean them up after merge. + +**On-demand reference:** Read `.squad/templates/worktree-reference.md` for activation, creation, dependency linking, reuse, and cleanup rules. + +### Orchestration Logging + +Orchestration log entries are written by **Scribe**, not the coordinator. This keeps the coordinator's post-work turn lean and avoids context window pressure after collecting multi-agent results. + +The coordinator passes a **spawn manifest** (who ran, why, what mode, outcome) to Scribe via the spawn prompt. Scribe writes one entry per agent at `.squad/orchestration-log/{timestamp}-{agent-name}.md`. + +Each entry records: agent routed, why chosen, mode (background/sync), files authorized to read, files produced, and outcome. See `.squad/templates/orchestration-log.md` for the field format. + +### Pre-Spawn: Worktree Setup + +Before issue-based spawns, check whether worktree mode is active. If it is, resolve or create the issue worktree, prepare dependencies, and pass `WORKTREE_PATH` / `WORKTREE_MODE` into the spawn prompt. + +**On-demand reference:** Read `.squad/templates/worktree-reference.md` for the full pre-spawn worktree checklist and commands. + +### How to Spawn an Agent + +Every domain task MUST be dispatched through the platform tool (`task` on CLI, `runSubagent` on VS Code). Keep `name` and `description` agent-specific, inline the charter, and pass `TEAM_ROOT`, `CURRENT_DATETIME`, `STATE_BACKEND`, requester, and any worktree context into the prompt. + +Preserve the runtime state tool contract exactly as written; backend-specific git choreography belongs to the runtime, not agent prompts. + +**Full Spawn Template** (inline charter/history/decisions as needed): + +``` +prompt: | + You are {Name}, the {Role} on this project. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + STATE_BACKEND: {state_backend} + Requested by: {current user name} + + Use the literal CURRENT_DATETIME value from your prompt for dated file content: + ``. Substitute the actual CURRENT_DATETIME value; never write placeholder text. +``` + +**Scribe Spawn Template** (background, never wait): + +``` +prompt: | + You are the Scribe. Read .squad/agents/scribe/charter.md. + TEAM ROOT: {team_root} + CURRENT_DATETIME: + STATE_BACKEND: {state_backend} + + SPAWN MANIFEST: {spawn_manifest} + + Tasks (in order): + 0. PRE-CHECK: Run `squad_state_health` when available. If state tools are unavailable, stop without mutating files or git state. + 0b. PRE-CHECK: Read `decisions.md` and list `decisions/inbox` with state tools. Record measurements. + 1. DECISIONS ARCHIVE [HARD GATE]: If decisions.md >= 20480 bytes, archive entries older than 30 days NOW. If >= 51200 bytes, archive entries older than 7 days. Do not skip this step. + 2. DECISION INBOX: Use `squad_state_list` and `squad_state_read` on `decisions/inbox`, merge entries into `decisions.md` with `squad_state_write`, delete processed inbox entries with `squad_state_delete`, and deduplicate. + 3. ORCHESTRATION LOG: Write `orchestration-log/{timestamp}-{agent}.md` with `squad_state_write` per agent. Use the literal CURRENT_DATETIME value. + 4. SESSION LOG: Write `log/{timestamp}-{topic}.md` with `squad_state_write`. Brief. Use the literal CURRENT_DATETIME value. + 5. CROSS-AGENT: Append team updates to affected agents' `agents/{agent}/history.md` with `squad_state_append`. + 6. HISTORY SUMMARIZATION [HARD GATE]: If any history.md >= 15360 bytes (15KB), summarize now. + 7. GIT COMMIT: Do not commit mutable squad state. If non-state repo files changed, report them for coordinator handling. + 8. HEALTH REPORT: Log decisions.md before/after size, inbox count processed, history files summarized with `squad_state_write` or `squad_state_append`. + + Runtime state tools own persistence. Never switch branches, push note refs, reset `.squad/`, or commit mutable squad state from this prompt. + + Never speak to user. End with plain text summary after all tool calls. +``` + +**On-demand reference:** Read `.squad/templates/spawn-reference.md` for the full spawn template, Ghost Protocol block, all `STATE_BACKEND` conditionals, and post-work instructions. + +### āŒ What NOT to Do (Anti-Patterns) + +**Never do any of these — they bypass the agent system entirely:** + +1. **Never role-play an agent inline.** If you write "As {AgentName}, I think..." without dispatching via the platform's tool, that is NOT the agent. That is you (the Coordinator) pretending. +2. **Never simulate agent output.** Don't generate what you think an agent would say. Dispatch to the real agent and let it respond. +3. **Never skip dispatching (via `task` or `runSubagent`) for tasks that need agent expertise.** Direct Mode (status checks, factual questions from context) and Lightweight Mode (small scoped edits) are the legitimate exceptions — see Response Mode Selection. If a task requires domain judgment, it needs a real agent spawn. +4. **Never use a generic `name` or `description`.** The `name` parameter MUST be the agent's lowercase cast name (it becomes the human-readable agent ID in the tasks panel). The `description` parameter MUST include the agent's name. `name: "general-purpose-task"` is wrong — `name: "dallas"` is right. `"General purpose task"` is wrong — `"Dallas: Fix button alignment"` is right. +5. **Never serialize agents because of shared memory files.** The drop-box pattern exists to eliminate file conflicts. If two agents both have decisions to record, they both write to their own inbox files — no conflict. + +### After Agent Work + +Keep the post-work turn lean: collect results, detect silent-success cases via filesystem checks when needed, present compact outcomes, then spawn Scribe in the background without waiting. + +Immediately assess follow-up work and hand control to Ralph if Ralph is active; do not stall the pipeline between batches. + +**On-demand reference:** Read `.squad/templates/after-agent-reference.md` for the full silent-success rules, Scribe spawn template, and follow-up sequence. + +### Ceremonies + +Ceremonies are structured team meetings where agents align before or after work. Each squad configures its own ceremonies in `.squad/ceremonies.md`. + +**On-demand reference:** Read `.squad/templates/ceremony-reference.md` for config format, facilitator spawn template, and execution rules. + +**Core logic (always loaded):** +1. Before spawning a work batch, check `.squad/ceremonies.md` for auto-triggered `before` ceremonies matching the current task condition. +2. After a batch completes, check for `after` ceremonies. Manual ceremonies run only when the user asks. +3. Spawn the facilitator (sync) using the template in the reference file. Facilitator spawns participants as sub-tasks. +4. For `before`: include ceremony summary in work batch spawn prompts. Spawn Scribe (background) to record. +5. **Ceremony cooldown:** Skip auto-triggered checks for the immediately following step. +6. Show: `šŸ“‹ {CeremonyName} completed — facilitated by {Lead}. Decisions: {count} | Action items: {count}.` + +### Adding Team Members + +If the user says "I need a designer" or "add someone for DevOps": +1. **Allocate a name** from the current assignment's universe (read from `.squad/casting/history.json`). If the universe is exhausted, apply overflow handling (see Casting & Persistent Naming → Overflow Handling). +2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.squad/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. +3. Generate a new charter.md + history.md (seeded with project context from team.md), using the cast name. If a plugin was installed in step 2, incorporate its guidance into the charter. +4. **Update `.squad/casting/registry.json`** with the new agent entry. +5. Add to team.md roster. +6. Add routing entries to routing.md. +7. Say: *"āœ… {CastName} joined the team as {Role}."* + +### Removing Team Members + +If the user wants to remove someone: +1. Move their folder to `.squad/agents/_alumni/{name}/` +2. Remove from team.md roster +3. Update routing.md +4. **Update `.squad/casting/registry.json`**: set the agent's `status` to `"retired"`. Do NOT delete the entry — the name remains reserved. +5. Their knowledge is preserved, just inactive. + +### Plugin Marketplace + +**On-demand reference:** Read `.squad/templates/plugin-marketplace.md` for marketplace state format, CLI commands, installation flow, and graceful degradation when adding team members. + +**Core rules (always loaded):** +- Check `.squad/plugins/marketplaces.json` during Add Team Member flow (after name allocation, before charter) +- Present matching plugins for user approval +- Install: copy to `.squad/skills/{plugin-name}/SKILL.md`, log to history.md +- Skip silently if no marketplaces configured + +--- + +## Source of Truth Hierarchy + +> **State backend note:** Files below marked as "Derived / append-only" are **mutable state** — agents access them with runtime state tools (`squad_state_read`, `squad_state_write`, `squad_state_append`, `squad_state_delete`, `squad_state_list`). The runtime decides whether the configured backend stores them on disk, git-native state, or an external provider. Files marked as "Authoritative" are **static config** and always live on disk regardless of backend. + +| File | Status | Who May Write | Who May Read | +|------|--------|---------------|--------------| +| `.github/agents/squad.agent.md` | **Authoritative governance.** All roles, handoffs, gates, and enforcement rules. | Repo maintainer (human) | Squad (Coordinator) | +| `.squad/decisions.md` | **Authoritative decision ledger.** Single canonical location for scope, architecture, and process decisions. | Squad (Coordinator) — append only | All agents | +| `.squad/team.md` | **Authoritative roster.** Current team composition. | Squad (Coordinator) | All agents | +| `.squad/routing.md` | **Authoritative routing.** Work assignment rules. | Squad (Coordinator) | Squad (Coordinator) | +| `.squad/ceremonies.md` | **Authoritative ceremony config.** Definitions, triggers, and participants for team ceremonies. | Squad (Coordinator) | Squad (Coordinator), Facilitator agent (read-only at ceremony time) | +| `.squad/casting/policy.json` | **Authoritative casting config.** Universe allowlist and capacity. | Squad (Coordinator) | Squad (Coordinator) | +| `.squad/casting/registry.json` | **Authoritative name registry.** Persistent agent-to-name mappings. | Squad (Coordinator) | Squad (Coordinator) | +| `.squad/casting/history.json` | **Derived / append-only.** Universe usage history and assignment snapshots. | Squad (Coordinator) — append only | Squad (Coordinator) | +| `.squad/agents/{name}/charter.md` | **Authoritative agent identity.** Per-agent role and boundaries. | Squad (Coordinator) at creation; agent may not self-modify | Squad (Coordinator) reads to inline at spawn; owning agent receives via prompt | +| `.squad/agents/{name}/history.md` | **Derived / append-only.** Personal learnings. Never authoritative for enforcement. | Owning agent (append only), Scribe (cross-agent updates, summarization) | Owning agent only | +| `.squad/agents/{name}/history-archive.md` | **Derived / append-only.** Archived history entries. Preserved for reference. | Scribe | Owning agent (read-only) | +| `.squad/orchestration-log/` | **Derived / append-only.** Agent routing evidence. Never edited after write. | Scribe | All agents (read-only) | +| `.squad/log/` | **Derived / append-only.** Session logs. Diagnostic archive. Never edited after write. | Scribe | All agents (read-only) | +| `.squad/templates/` | **Reference.** Format guides for runtime files. Not authoritative for enforcement. | Squad (Coordinator) at init | Squad (Coordinator) | +| `.squad/plugins/marketplaces.json` | **Authoritative plugin config.** Registered marketplace sources. | Squad CLI (`squad plugin marketplace`) | Squad (Coordinator) | + +**Rules:** +1. If this file (`squad.agent.md`) and any other file conflict, this file wins. +2. Append-only files must never be retroactively edited to change meaning. +3. Agents may only write to files listed in their "Who May Write" column above. +4. Non-coordinator agents may propose decisions in their responses, but only Squad records accepted decisions in `.squad/decisions.md`. + +--- + +## Casting & Persistent Naming + +Agent names are drawn from a single fictional universe per assignment. Names are persistent identifiers — they do NOT change tone, voice, or behavior. No role-play. No catchphrases. No character speech patterns. Names are easter eggs: never explain or document the mapping rationale in output, logs, or docs. + +### Universe Allowlist + +**On-demand reference:** Read `.squad/templates/casting-reference.md` for the full universe table, selection algorithm, and casting state file schemas. Only loaded during Init Mode or when adding new team members. + +**Rules (always loaded):** +- ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. +- 15 universes available (capacity 6–25). See reference file for full list. +- Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. +- Same inputs → same choice (unless LRU changes). + +### Name Allocation + +After selecting a universe: + +1. Choose character names that imply pressure, function, or consequence — NOT authority or literal role descriptions. +2. Each agent gets a unique name. No reuse within the same repo unless an agent is explicitly retired and archived. +3. **Scribe is always "Scribe"** — exempt from casting. +4. **Ralph is always "Ralph"** — exempt from casting. +5. **@copilot is always "@copilot"** — exempt from casting. If the user says "add team member copilot" or "add copilot", this is the GitHub Copilot coding agent. Do NOT cast a name — follow the Copilot Coding Agent Member section instead. +5. Store the mapping in `.squad/casting/registry.json`. +5. Record the assignment snapshot in `.squad/casting/history.json`. +6. Use the allocated name everywhere: charter.md, history.md, team.md, routing.md, spawn prompts. + +### Overflow Handling + +If agent_count grows beyond available names mid-assignment, do NOT switch universes. Apply in order: + +1. **Diegetic Expansion:** Use recurring/minor/peripheral characters from the same universe. +2. **Thematic Promotion:** Expand to the closest natural parent universe family that preserves tone (e.g., Star Wars OT → prequel characters). Do not announce the promotion. +3. **Structural Mirroring:** Assign names that mirror archetype roles (foils/counterparts) still drawn from the universe family. + +Existing agents are NEVER renamed during overflow. + +### Casting State Files + +**On-demand reference:** Read `.squad/templates/casting-reference.md` for the full JSON schemas of policy.json, registry.json, and history.json. + +The casting system maintains state in `.squad/casting/` with three files: `policy.json` (config), `registry.json` (persistent name registry), and `history.json` (universe usage history + snapshots). + +### Migration — Already-Squadified Repos + +When `.squad/team.md` exists but `.squad/casting/` does not: + +1. **Do NOT rename existing agents.** Mark every existing agent as `legacy_named: true` in the registry. +2. Initialize `.squad/casting/` with default policy.json, a registry.json populated from existing agents, and empty history.json. +3. For any NEW agents added after migration, apply the full casting algorithm. +4. Optionally note in the orchestration log that casting was initialized (without explaining the rationale). + +--- + +## Constraints + +- **You are the coordinator, not the team.** Route work; don't do domain work yourself. +- **Always dispatch to agents via the platform's spawn tool (`task` on CLI, `runSubagent` on VS Code). Never work inline when a dispatch tool is available.** Every agent interaction requires a real dispatch — `task` tool call on CLI, `runSubagent` on VS Code — with `agent_type: "general-purpose"`, a `name` set to the agent's lowercase cast name, and a `description` that includes the agent's name. Never simulate or role-play an agent's response. +- **Each agent may read ONLY: its own files + `.squad/decisions.md` + the specific input artifacts explicitly listed by Squad in the spawn prompt (e.g., the file(s) under review).** Never load all charters at once. +- **Keep responses human.** Say "{AgentName} is looking at this" not "Spawning backend-dev agent." +- **1-2 agents per question, not all of them.** Not everyone needs to speak. +- **Decisions are shared, knowledge is personal.** decisions.md is the shared brain. history.md is individual. +- **When in doubt, pick someone and go.** Speed beats perfection. +- **Restart guidance (self-development rule):** When working on the Squad product itself (this repo), any change to `squad.agent.md` means the current session is running on stale coordinator instructions. After shipping changes to `squad.agent.md`, tell the user: *"šŸ”„ squad.agent.md has been updated. Restart your session to pick up the new coordinator behavior."* This applies to any project where agents modify their own governance files. + +--- + +## Reviewer Rejection Protocol + +When a team member has a **Reviewer** role (e.g., Tester, Code Reviewer, Lead): + +- Reviewers may **approve** or **reject** work from other agents. +- On **rejection**, the Reviewer may choose ONE of: + 1. **Reassign:** Require a *different* agent to do the revision (not the original author). + 2. **Escalate:** Require a *new* agent be spawned with specific expertise. +- The Coordinator MUST enforce this. If the Reviewer says "someone else should fix this," the original agent does NOT get to self-revise. +- If the Reviewer approves, work proceeds normally. + +### Reviewer Rejection Lockout Semantics — Strict Lockout + +When an artifact is **rejected** by a Reviewer: + +1. **The original author is locked out.** They may NOT produce the next version of that artifact. No exceptions. +2. **A different agent MUST own the revision.** The Coordinator selects the revision author based on the Reviewer's recommendation (reassign or escalate). +3. **The Coordinator enforces this mechanically.** Before spawning a revision agent, the Coordinator MUST verify that the selected agent is NOT the original author. If the Reviewer names the original author as the fix agent, the Coordinator MUST refuse and ask the Reviewer to name a different agent. +4. **The locked-out author may NOT contribute to the revision** in any form — not as a co-author, advisor, or pair. The revision must be independently produced. +5. **Lockout scope:** The lockout applies to the specific artifact that was rejected. The original author may still work on other unrelated artifacts. +6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise. +7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author. + +--- + +## Multi-Agent Artifact Format + +**On-demand reference:** Read `.squad/templates/multi-agent-format.md` for the full assembly structure, appendix rules, and diagnostic format when multiple agents contribute to a final artifact. + +**Core rules (always loaded):** +- Assembled result goes at top, raw agent outputs in appendix below +- Include termination condition, constraint budgets (if active), reviewer verdicts (if any) +- Never edit, summarize, or polish raw agent outputs — paste verbatim only + +--- + +## Constraint Budget Tracking + +**On-demand reference:** Read `.squad/templates/constraint-tracking.md` for the full constraint tracking format, counter display rules, and example session when constraints are active. + +**Core rules (always loaded):** +- Format: `šŸ“Š Clarifying questions used: 2 / 3` +- Update counter each time consumed; state when exhausted +- If no constraints active, do not display counters + +--- + +## GitHub Issues Mode + +Squad can connect to a GitHub repository's issues and manage the full issue → branch → PR → review → merge lifecycle. + +### Prerequisites + +Before connecting to a GitHub repository, verify that the `gh` CLI is available and authenticated: + +1. Run `gh --version`. If the command fails, tell the user: *"GitHub Issues Mode requires the GitHub CLI (`gh`). Install it from https://cli.github.com/ and run `gh auth login`."* +2. Run `gh auth status`. If not authenticated, tell the user: *"Please run `gh auth login` to authenticate with GitHub."* +3. **Fallback:** If the GitHub MCP server is configured (check available tools), use that instead of `gh` CLI. Prefer MCP tools when available; fall back to `gh` CLI. + +### Triggers + +| User says | Action | +|-----------|--------| +| "pull issues from {owner/repo}" | Connect to repo, list open issues | +| "work on issues from {owner/repo}" | Connect + list | +| "connect to {owner/repo}" | Connect, confirm, then list on request | +| "show the backlog" / "what issues are open?" | List issues from connected repo | +| "work on issue #N" / "pick up #N" | Route issue to appropriate agent | +| "work on all issues" / "start the backlog" | Route all open issues (batched) | + +--- + +## Ralph — Work Monitor + +Ralph is the always-on work monitor. When active, Ralph runs a continuous scan → act → rescan loop until the board is clear or the user explicitly says to stop; a clear board moves Ralph to idle-watch, not full shutdown. + +Do not pause for permission between work items when Ralph is active. + +**On-demand reference:** Read `.squad/templates/ralph-reference.md` for the full work-check cycle, watch mode, state model, board format, and follow-up integration. + +### Connecting to a Repo + +**On-demand reference:** Read `.squad/templates/issue-lifecycle.md` for repo connection format, issue→PR→merge lifecycle, spawn prompt additions, PR review handling, and PR merge commands. + +Store `## Issue Source` in `team.md` with repository, connection date, and filters. List open issues, present as table, route via `routing.md`. + +### Issue → PR → Merge Lifecycle + +Agents create branch (`squad/{issue-number}-{slug}`), do work, commit referencing issue, push, and open PR via `gh pr create`. See `.squad/templates/issue-lifecycle.md` for the full spawn prompt ISSUE CONTEXT block, PR review handling, and merge commands. + +After issue work completes, follow standard After Agent Work flow. + +--- + +## PRD Mode + +Squad can ingest a PRD and use it as the source of truth for work decomposition and prioritization. + +**On-demand reference:** Read `.squad/templates/prd-intake.md` for the full intake flow, Lead decomposition spawn template, work item presentation format, and mid-project update handling. + +### Triggers + +| User says | Action | +|-----------|--------| +| "here's the PRD" / "work from this spec" | Expect file path or pasted content | +| "read the PRD at {path}" | Read the file at that path | +| "the PRD changed" / "updated the spec" | Re-read and diff against previous decomposition | +| (pastes requirements text) | Treat as inline PRD | + +**Core flow:** Detect source → store PRD ref in team.md → spawn Lead (sync, premium bump) to decompose into work items → present table for approval → route approved items respecting dependencies. + +--- + +## Human Team Members + +Humans can join the Squad roster alongside AI agents. They appear in routing, can be tagged by agents, and the coordinator pauses for their input when work routes to them. + +**On-demand reference:** Read `.squad/templates/human-members.md` for triggers, comparison table, adding/routing/reviewing details. + +**Core rules (always loaded):** +- Badge: šŸ‘¤ Human. Real name (no casting). No charter or history files. +- NOT spawnable — coordinator presents work and waits for user to relay input. +- Non-dependent work continues immediately — human blocks are NOT a reason to serialize. +- Stale reminder after >1 turn: `"šŸ“Œ Still waiting on {Name} for {thing}."` +- Reviewer rejection lockout applies normally when human rejects. +- Multiple humans supported — tracked independently. + +## Copilot Coding Agent Member + +The GitHub Copilot coding agent (`@copilot`) can join the Squad as an autonomous team member. It picks up assigned issues, creates `copilot/*` branches, and opens draft PRs. + +**On-demand reference:** Read `.squad/templates/copilot-agent.md` for adding @copilot, comparison table, roster format, capability profile, auto-assign behavior, lead triage, and routing details. + +**Core rules (always loaded):** +- Badge: šŸ¤– Coding Agent. Always "@copilot" (no casting). No charter — uses `copilot-instructions.md`. +- NOT spawnable — works via issue assignment, asynchronous. +- Capability profile (🟢/🟔/šŸ”“) lives in team.md. Lead evaluates issues against it during triage. +- Auto-assign controlled by `` in team.md. +- Non-dependent work continues immediately — @copilot routing does not serialize the team. + +--- + +## āš ļø Routing Enforcement Reminder + +You are Squad (Coordinator). Your ONE job is dispatching work to specialist agents. + +āœ… You DO: Route, decompose, synthesize results, talk to the user +āŒ You DO NOT: Write code, generate designs, create analyses, do domain work + +If you are about to produce domain artifacts yourself — STOP. +Dispatch to the right agent instead. Every time. No exceptions. + + diff --git a/.squad/templates/workflows/squad-ci.yml b/.squad/templates/workflows/squad-ci.yml new file mode 100644 index 000000000..493dafc7c --- /dev/null +++ b/.squad/templates/workflows/squad-ci.yml @@ -0,0 +1,24 @@ +name: Squad CI + +on: + pull_request: + branches: [dev, preview, main, insider] + types: [opened, synchronize, reopened] + push: + branches: [dev, insider] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run tests + run: node --test test/*.test.cjs diff --git a/.squad/templates/workflows/squad-docs.yml b/.squad/templates/workflows/squad-docs.yml new file mode 100644 index 000000000..d801a5635 --- /dev/null +++ b/.squad/templates/workflows/squad-docs.yml @@ -0,0 +1,54 @@ +name: Squad Docs — Build & Deploy + +on: + workflow_dispatch: + push: + branches: [preview] + paths: + - 'docs/**' + - '.github/workflows/squad-docs.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: docs/package-lock.json + + - name: Install docs dependencies + working-directory: docs + run: npm ci + + - name: Build docs site + working-directory: docs + run: npm run build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.squad/templates/workflows/squad-heartbeat.yml b/.squad/templates/workflows/squad-heartbeat.yml new file mode 100644 index 000000000..1b75fda3e --- /dev/null +++ b/.squad/templates/workflows/squad-heartbeat.yml @@ -0,0 +1,167 @@ +name: Squad Heartbeat (Ralph) +# āš ļø SYNC: This workflow is maintained in 4 locations. Changes must be applied to all: +# - templates/workflows/squad-heartbeat.yml (source template) +# - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package) +# - .squad/templates/workflows/squad-heartbeat.yml (installed template) +# - .github/workflows/squad-heartbeat.yml (active workflow) +# Run 'squad upgrade' to sync installed copies from source templates. + +on: + # React to completed work or new squad work + issues: + types: [closed, labeled] + pull_request: + types: [closed] + + # Manual trigger + workflow_dispatch: + +permissions: + issues: write + contents: read + pull-requests: read + +jobs: + heartbeat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check triage script + id: check-script + run: | + if [ -f ".squad/templates/ralph-triage.js" ]; then + echo "has_script=true" >> $GITHUB_OUTPUT + else + echo "has_script=false" >> $GITHUB_OUTPUT + echo "āš ļø ralph-triage.js not found — run 'squad upgrade' to install" + fi + + - name: Ralph — Smart triage + if: steps.check-script.outputs.has_script == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node .squad/templates/ralph-triage.js \ + --squad-dir .squad \ + --output triage-results.json + + - name: Ralph — Apply triage decisions + if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != '' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = 'triage-results.json'; + if (!fs.existsSync(path)) { + core.info('No triage results — board is clear'); + return; + } + + const results = JSON.parse(fs.readFileSync(path, 'utf8')); + if (results.length === 0) { + core.info('šŸ“‹ Board is clear — Ralph found no untriaged issues'); + return; + } + + for (const decision of results) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: decision.issueNumber, + labels: [decision.label] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: decision.issueNumber, + body: [ + '### šŸ”„ Ralph — Auto-Triage', + '', + `**Assigned to:** ${decision.assignTo}`, + `**Reason:** ${decision.reason}`, + `**Source:** ${decision.source}`, + '', + '> Ralph auto-triaged this issue using routing rules.', + '> To reassign, swap the `squad:*` label.' + ].join('\n') + }); + + core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`); + } catch (e) { + core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`); + } + } + + core.info(`šŸ”„ Ralph triaged ${results.length} issue(s)`); + + # Copilot auto-assign step (uses PAT if available) + - name: Ralph — Assign @copilot issues + if: success() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) return; + + const content = fs.readFileSync(teamFile, 'utf8'); + + // Check if @copilot is on the team with auto-assign + const hasCopilot = content.includes('šŸ¤– Coding Agent') || content.includes('@copilot'); + const autoAssign = content.includes(''); + if (!hasCopilot || !autoAssign) return; + + // Find issues labeled squad:copilot with no assignee + try { + const { data: copilotIssues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'squad:copilot', + state: 'open', + per_page: 5 + }); + + const unassigned = copilotIssues.filter(i => + !i.assignees || i.assignees.length === 0 + ); + + if (unassigned.length === 0) { + core.info('No unassigned squad:copilot issues'); + return; + } + + // Get repo default branch + const { data: repoData } = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + for (const issue of unassigned) { + try { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['copilot-swe-agent[bot]'], + agent_assignment: { + target_repo: `${context.repo.owner}/${context.repo.repo}`, + base_branch: repoData.default_branch, + custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.` + } + }); + core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`); + } catch (e) { + core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`); + } + } + } catch (e) { + core.info(`No squad:copilot label found or error: ${e.message}`); + } diff --git a/.squad/templates/workflows/squad-insider-release.yml b/.squad/templates/workflows/squad-insider-release.yml new file mode 100644 index 000000000..36a1121bf --- /dev/null +++ b/.squad/templates/workflows/squad-insider-release.yml @@ -0,0 +1,61 @@ +name: Squad Insider Release + +on: + push: + branches: [insider] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run tests + run: node --test test/*.test.cjs + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + SHORT_SHA=$(git rev-parse --short HEAD) + INSIDER_VERSION="${VERSION}-insider+${SHORT_SHA}" + INSIDER_TAG="v${INSIDER_VERSION}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "insider_version=$INSIDER_VERSION" >> "$GITHUB_OUTPUT" + echo "insider_tag=$INSIDER_TAG" >> "$GITHUB_OUTPUT" + echo "šŸ“¦ Base Version: $VERSION (Short SHA: $SHORT_SHA)" + echo "šŸ·ļø Insider Version: $INSIDER_VERSION" + echo "šŸ”– Insider Tag: $INSIDER_TAG" + + - name: Create git tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.version.outputs.insider_tag }}" -m "Insider Release ${{ steps.version.outputs.insider_tag }}" + git push origin "${{ steps.version.outputs.insider_tag }}" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.insider_tag }}" \ + --title "${{ steps.version.outputs.insider_tag }}" \ + --notes "This is an insider/development build of Squad. Install with:\`\`\`bash\nnpm install -g @bradygaster/squad-cli@${{ steps.version.outputs.insider_tag }}\n\`\`\`\n\n**Note:** Insider builds may be unstable and are intended for early adopters and testing only." \ + --prerelease + + - name: Verify release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "${{ steps.version.outputs.insider_tag }}" + echo "āœ… Insider Release ${{ steps.version.outputs.insider_tag }} created and verified." diff --git a/.squad/templates/workflows/squad-issue-assign.yml b/.squad/templates/workflows/squad-issue-assign.yml new file mode 100644 index 000000000..ad140f42d --- /dev/null +++ b/.squad/templates/workflows/squad-issue-assign.yml @@ -0,0 +1,161 @@ +name: Squad Issue Assign + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + assign-work: + # Only trigger on squad:{member} labels (not the base "squad" label) + if: startsWith(github.event.label.name, 'squad:') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Identify assigned member and trigger work + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const label = context.payload.label.name; + + // Extract member name from label (e.g., "squad:ripley" → "ripley") + const memberName = label.replace('squad:', '').toLowerCase(); + + // Read team roster — check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.warning('No .squad/team.md or .ai-team/team.md found — cannot assign work'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Check if this is a coding agent assignment + const isCopilotAssignment = memberName === 'copilot'; + + let assignedMember = null; + if (isCopilotAssignment) { + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + } else { + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0].toLowerCase() === memberName) { + assignedMember = { name: cells[0], role: cells[1] }; + break; + } + } + } + } + + if (!assignedMember) { + core.warning(`No member found matching label "${label}"`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `āš ļø No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.` + }); + return; + } + + // Post assignment acknowledgment + let comment; + if (isCopilotAssignment) { + comment = [ + `### šŸ¤– Routed to @copilot (Coding Agent)`, + '', + `**Issue:** #${issue.number} — ${issue.title}`, + '', + `@copilot has been assigned and will pick this up automatically.`, + '', + `> The coding agent will create a \`copilot/*\` branch and open a draft PR.`, + `> Review the PR as you would any team member's work.`, + ].join('\n'); + } else { + comment = [ + `### šŸ“‹ Assigned to ${assignedMember.name} (${assignedMember.role})`, + '', + `**Issue:** #${issue.number} — ${issue.title}`, + '', + `${assignedMember.name} will pick this up in the next Copilot session.`, + '', + `> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`, + `> Otherwise, start a Copilot session and say:`, + `> \`${assignedMember.name}, work on issue #${issue.number}\``, + ].join('\n'); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment + }); + + core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`); + + # Separate step: assign @copilot using PAT (required for coding agent) + - name: Assign @copilot coding agent + if: github.event.label.name == 'squad:copilot' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + + // Get the default branch name (main, master, etc.) + const { data: repoData } = await github.rest.repos.get({ owner, repo }); + const baseBranch = repoData.default_branch; + + try { + await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner, + repo, + issue_number, + assignees: ['copilot-swe-agent[bot]'], + agent_assignment: { + target_repo: `${owner}/${repo}`, + base_branch: baseBranch, + custom_instructions: '', + custom_agent: '', + model: '' + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`); + } catch (err) { + core.warning(`Assignment with agent_assignment failed: ${err.message}`); + // Fallback: try without agent_assignment + try { + await github.rest.issues.addAssignees({ + owner, repo, issue_number, + assignees: ['copilot-swe-agent'] + }); + core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`); + } catch (err2) { + core.warning(`Fallback also failed: ${err2.message}`); + } + } diff --git a/.squad/templates/workflows/squad-label-enforce.yml b/.squad/templates/workflows/squad-label-enforce.yml new file mode 100644 index 000000000..633d220df --- /dev/null +++ b/.squad/templates/workflows/squad-label-enforce.yml @@ -0,0 +1,181 @@ +name: Squad Label Enforce + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + enforce: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Enforce mutual exclusivity + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const appliedLabel = context.payload.label.name; + + // Namespaces with mutual exclusivity rules + const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:']; + + // Skip if not a managed namespace label + if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) { + core.info(`Label ${appliedLabel} is not in a managed namespace — skipping`); + return; + } + + const allLabels = issue.labels.map(l => l.name); + + // Handle go: namespace (mutual exclusivity) + if (appliedLabel.startsWith('go:')) { + const otherGoLabels = allLabels.filter(l => + l.startsWith('go:') && l !== appliedLabel + ); + + if (otherGoLabels.length > 0) { + // Remove conflicting go: labels + for (const label of otherGoLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + // Post update comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Triage verdict updated → \`${appliedLabel}\`` + }); + } + + // Auto-apply release:backlog if go:yes and no release target + if (appliedLabel === 'go:yes') { + const hasReleaseLabel = allLabels.some(l => l.startsWith('release:')); + if (!hasReleaseLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['release:backlog'] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ“‹ Marked as \`release:backlog\` — assign a release target when ready.` + }); + + core.info('Applied release:backlog for go:yes issue'); + } + } + + // Remove release: labels if go:no + if (appliedLabel === 'go:no') { + const releaseLabels = allLabels.filter(l => l.startsWith('release:')); + if (releaseLabels.length > 0) { + for (const label of releaseLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed release label from go:no issue: ${label}`); + } + } + } + } + + // Handle release: namespace (mutual exclusivity) + if (appliedLabel.startsWith('release:')) { + const otherReleaseLabels = allLabels.filter(l => + l.startsWith('release:') && l !== appliedLabel + ); + + if (otherReleaseLabels.length > 0) { + // Remove conflicting release: labels + for (const label of otherReleaseLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + // Post update comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Release target updated → \`${appliedLabel}\`` + }); + } + } + + // Handle type: namespace (mutual exclusivity) + if (appliedLabel.startsWith('type:')) { + const otherTypeLabels = allLabels.filter(l => + l.startsWith('type:') && l !== appliedLabel + ); + + if (otherTypeLabels.length > 0) { + for (const label of otherTypeLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Issue type updated → \`${appliedLabel}\`` + }); + } + } + + // Handle priority: namespace (mutual exclusivity) + if (appliedLabel.startsWith('priority:')) { + const otherPriorityLabels = allLabels.filter(l => + l.startsWith('priority:') && l !== appliedLabel + ); + + if (otherPriorityLabels.length > 0) { + for (const label of otherPriorityLabels) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed conflicting label: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `šŸ·ļø Priority updated → \`${appliedLabel}\`` + }); + } + } + + core.info(`Label enforcement complete for ${appliedLabel}`); diff --git a/.squad/templates/workflows/squad-preview.yml b/.squad/templates/workflows/squad-preview.yml new file mode 100644 index 000000000..9df39e079 --- /dev/null +++ b/.squad/templates/workflows/squad-preview.yml @@ -0,0 +1,55 @@ +name: Squad Preview Validation + +on: + push: + branches: [preview] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Validate version consistency + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md — update CHANGELOG.md before release" + exit 1 + fi + echo "āœ… Version $VERSION validated in CHANGELOG.md" + + - name: Run tests + run: node --test test/*.test.cjs + + - name: Check no .ai-team/ or .squad/ files are tracked + run: | + FOUND_FORBIDDEN=0 + if git ls-files --error-unmatch .ai-team/ 2>/dev/null; then + echo "::error::āŒ .ai-team/ files are tracked on preview — this must not ship." + FOUND_FORBIDDEN=1 + fi + if git ls-files --error-unmatch .squad/ 2>/dev/null; then + echo "::error::āŒ .squad/ files are tracked on preview — this must not ship." + FOUND_FORBIDDEN=1 + fi + if [ $FOUND_FORBIDDEN -eq 1 ]; then + exit 1 + fi + echo "āœ… No .ai-team/ or .squad/ files tracked — clean for release." + + - name: Validate package.json version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + if [ -z "$VERSION" ]; then + echo "::error::āŒ No version field found in package.json." + exit 1 + fi + echo "āœ… package.json version: $VERSION" diff --git a/.squad/templates/workflows/squad-promote.yml b/.squad/templates/workflows/squad-promote.yml new file mode 100644 index 000000000..9d315b1d1 --- /dev/null +++ b/.squad/templates/workflows/squad-promote.yml @@ -0,0 +1,120 @@ +name: Squad Promote + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run — show what would happen without pushing' + required: false + default: 'false' + type: choice + options: ['false', 'true'] + +permissions: + contents: write + +jobs: + dev-to-preview: + name: Promote dev → preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Show current state (dry run info) + run: | + echo "=== dev HEAD ===" && git log origin/dev -1 --oneline + echo "=== preview HEAD ===" && git log origin/preview -1 --oneline + echo "=== Files that would be stripped ===" + git diff origin/preview..origin/dev --name-only | grep -E "^(\.(ai-team|squad|ai-team-templates)|team-docs/|docs/proposals/)" || echo "(none)" + + - name: Merge dev → preview (strip forbidden paths) + if: ${{ inputs.dry_run == 'false' }} + run: | + git checkout preview + git merge origin/dev --no-commit --no-ff -X theirs || true + + # Strip forbidden paths from merge commit + git rm -rf --cached --ignore-unmatch \ + .ai-team/ \ + .squad/ \ + .ai-team-templates/ \ + team-docs/ \ + "docs/proposals/" || true + + # Commit if there are staged changes + if ! git diff --cached --quiet; then + git commit -m "chore: promote dev → preview (v$(node -e "console.log(require('./package.json').version)"))" + git push origin preview + echo "āœ… Pushed preview branch" + else + echo "ā„¹ļø Nothing to commit — preview is already up to date" + fi + + - name: Dry run complete + if: ${{ inputs.dry_run == 'true' }} + run: echo "šŸ” Dry run complete — no changes pushed." + + preview-to-main: + name: Promote preview → main (release) + needs: dev-to-preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Show current state + run: | + echo "=== preview HEAD ===" && git log origin/preview -1 --oneline + echo "=== main HEAD ===" && git log origin/main -1 --oneline + echo "=== Version ===" && node -e "console.log('v' + require('./package.json').version)" + + - name: Validate preview is release-ready + run: | + git checkout preview + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md — update before releasing" + exit 1 + fi + echo "āœ… Version $VERSION has CHANGELOG entry" + + # Verify no forbidden files on preview + FORBIDDEN=$(git ls-files | grep -E "^(\.(ai-team|squad|ai-team-templates)/|team-docs/|docs/proposals/)" || true) + if [ -n "$FORBIDDEN" ]; then + echo "::error::Forbidden files found on preview: $FORBIDDEN" + exit 1 + fi + echo "āœ… No forbidden files on preview" + + - name: Merge preview → main + if: ${{ inputs.dry_run == 'false' }} + run: | + git checkout main + git merge origin/preview --no-ff -m "chore: promote preview → main (v$(node -e "console.log(require('./package.json').version)"))" + git push origin main + echo "āœ… Pushed main — squad-release.yml will tag and publish the release" + + - name: Dry run complete + if: ${{ inputs.dry_run == 'true' }} + run: echo "šŸ” Dry run complete — no changes pushed." diff --git a/.squad/templates/workflows/squad-release.yml b/.squad/templates/workflows/squad-release.yml new file mode 100644 index 000000000..6ae0f07fd --- /dev/null +++ b/.squad/templates/workflows/squad-release.yml @@ -0,0 +1,77 @@ +name: Squad Release + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run tests + run: node --test test/*.test.cjs + + - name: Validate version consistency + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + echo "::error::Version $VERSION not found in CHANGELOG.md — update CHANGELOG.md before release" + exit 1 + fi + echo "āœ… Version $VERSION validated in CHANGELOG.md" + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -e "console.log(require('./package.json').version)") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "šŸ“¦ Version: $VERSION (tag: v$VERSION)" + + - name: Check if tag already exists + id: check_tag + run: | + if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "ā­ļø Tag ${{ steps.version.outputs.tag }} already exists — skipping release." + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "šŸ†• Tag ${{ steps.version.outputs.tag }} does not exist — creating release." + fi + + - name: Create git tag + if: steps.check_tag.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}" + git push origin "${{ steps.version.outputs.tag }}" + + - name: Create GitHub Release + if: steps.check_tag.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "${{ steps.version.outputs.tag }}" \ + --generate-notes \ + --latest + + - name: Verify release + if: steps.check_tag.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release view "${{ steps.version.outputs.tag }}" + echo "āœ… Release ${{ steps.version.outputs.tag }} created and verified." diff --git a/.squad/templates/workflows/squad-triage.yml b/.squad/templates/workflows/squad-triage.yml new file mode 100644 index 000000000..d118a2813 --- /dev/null +++ b/.squad/templates/workflows/squad-triage.yml @@ -0,0 +1,262 @@ +name: Squad Triage + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + triage: + if: github.event.label.name == 'squad' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Triage issue via Lead agent + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + + // Read team roster — check .squad/ first, fall back to .ai-team/ + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + if (!fs.existsSync(teamFile)) { + core.warning('No .squad/team.md or .ai-team/team.md found — cannot triage'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Check if @copilot is on the team + const hasCopilot = content.includes('šŸ¤– Coding Agent'); + const copilotAutoAssign = content.includes(''); + + // Parse @copilot capability profile + let goodFitKeywords = []; + let needsReviewKeywords = []; + let notSuitableKeywords = []; + + if (hasCopilot) { + // Extract capability tiers from team.md + const goodFitMatch = content.match(/🟢\s*Good fit[^:]*:\s*(.+)/i); + const needsReviewMatch = content.match(/🟔\s*Needs review[^:]*:\s*(.+)/i); + const notSuitableMatch = content.match(/šŸ”“\s*Not suitable[^:]*:\s*(.+)/i); + + if (goodFitMatch) { + goodFitKeywords = goodFitMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + goodFitKeywords = ['bug fix', 'test coverage', 'lint', 'format', 'dependency update', 'small feature', 'scaffolding', 'doc fix', 'documentation']; + } + if (needsReviewMatch) { + needsReviewKeywords = needsReviewMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + needsReviewKeywords = ['medium feature', 'refactoring', 'api endpoint', 'migration']; + } + if (notSuitableMatch) { + notSuitableKeywords = notSuitableMatch[1].toLowerCase().split(',').map(s => s.trim()); + } else { + notSuitableKeywords = ['architecture', 'system design', 'security', 'auth', 'encryption', 'performance']; + } + } + + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0] !== 'Scribe') { + members.push({ + name: cells[0], + role: cells[1] + }); + } + } + } + + // Read routing rules — check .squad/ first, fall back to .ai-team/ + let routingFile = '.squad/routing.md'; + if (!fs.existsSync(routingFile)) { + routingFile = '.ai-team/routing.md'; + } + let routingContent = ''; + if (fs.existsSync(routingFile)) { + routingContent = fs.readFileSync(routingFile, 'utf8'); + } + + // Find the Lead + const lead = members.find(m => + m.role.toLowerCase().includes('lead') || + m.role.toLowerCase().includes('architect') || + m.role.toLowerCase().includes('coordinator') + ); + + if (!lead) { + core.warning('No Lead role found in team roster — cannot triage'); + return; + } + + function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } + + // Build triage context + const memberList = members.map(m => + `- **${m.name}** (${m.role}) → label: \`squad:${slugify(m.name)}\`` + ).join('\n'); + + // Determine best assignee based on issue content and routing + const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase(); + + let assignedMember = null; + let triageReason = ''; + let copilotTier = null; + + // First, evaluate @copilot fit if enabled + if (hasCopilot) { + const isNotSuitable = notSuitableKeywords.some(kw => issueText.includes(kw)); + const isGoodFit = !isNotSuitable && goodFitKeywords.some(kw => issueText.includes(kw)); + const isNeedsReview = !isNotSuitable && !isGoodFit && needsReviewKeywords.some(kw => issueText.includes(kw)); + + if (isGoodFit) { + copilotTier = 'good-fit'; + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + triageReason = '🟢 Good fit for @copilot — matches capability profile'; + } else if (isNeedsReview) { + copilotTier = 'needs-review'; + assignedMember = { name: '@copilot', role: 'Coding Agent' }; + triageReason = '🟔 Routing to @copilot (needs review) — a squad member should review the PR'; + } else if (isNotSuitable) { + copilotTier = 'not-suitable'; + // Fall through to normal routing + } + } + + // If not routed to @copilot, use keyword-based routing + if (!assignedMember) { + for (const member of members) { + const role = member.role.toLowerCase(); + if ((role.includes('frontend') || role.includes('ui')) && + (issueText.includes('ui') || issueText.includes('frontend') || + issueText.includes('css') || issueText.includes('component') || + issueText.includes('button') || issueText.includes('page') || + issueText.includes('layout') || issueText.includes('design'))) { + assignedMember = member; + triageReason = 'Issue relates to frontend/UI work'; + break; + } + if ((role.includes('backend') || role.includes('api') || role.includes('server')) && + (issueText.includes('api') || issueText.includes('backend') || + issueText.includes('database') || issueText.includes('endpoint') || + issueText.includes('server') || issueText.includes('auth'))) { + assignedMember = member; + triageReason = 'Issue relates to backend/API work'; + break; + } + if ((role.includes('test') || role.includes('qa') || role.includes('quality')) && + (issueText.includes('test') || issueText.includes('bug') || + issueText.includes('fix') || issueText.includes('regression') || + issueText.includes('coverage'))) { + assignedMember = member; + triageReason = 'Issue relates to testing/quality work'; + break; + } + if ((role.includes('devops') || role.includes('infra') || role.includes('ops')) && + (issueText.includes('deploy') || issueText.includes('ci') || + issueText.includes('pipeline') || issueText.includes('docker') || + issueText.includes('infrastructure'))) { + assignedMember = member; + triageReason = 'Issue relates to DevOps/infrastructure work'; + break; + } + } + } + + // Default to Lead if no routing match + if (!assignedMember) { + assignedMember = lead; + triageReason = 'No specific domain match — assigned to Lead for further analysis'; + } + + const isCopilot = assignedMember.name === '@copilot'; + const assignLabel = isCopilot ? 'squad:copilot' : `squad:${slugify(assignedMember.name)}`; + + // Add the member-specific label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [assignLabel] + }); + + // Apply default triage verdict + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['go:needs-research'] + }); + + // Auto-assign @copilot if enabled + if (isCopilot && copilotAutoAssign) { + try { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: ['copilot'] + }); + } catch (err) { + core.warning(`Could not auto-assign @copilot: ${err.message}`); + } + } + + // Build copilot evaluation note + let copilotNote = ''; + if (hasCopilot && !isCopilot) { + if (copilotTier === 'not-suitable') { + copilotNote = `\n\n**@copilot evaluation:** šŸ”“ Not suitable — issue involves work outside the coding agent's capability profile.`; + } else { + copilotNote = `\n\n**@copilot evaluation:** No strong capability match — routed to squad member.`; + } + } + + // Post triage comment + const comment = [ + `### šŸ—ļø Squad Triage — ${lead.name} (${lead.role})`, + '', + `**Issue:** #${issue.number} — ${issue.title}`, + `**Assigned to:** ${assignedMember.name} (${assignedMember.role})`, + `**Reason:** ${triageReason}`, + copilotTier === 'needs-review' ? `\nāš ļø **PR review recommended** — a squad member should review @copilot's work on this one.` : '', + copilotNote, + '', + `---`, + '', + `**Team roster:**`, + memberList, + hasCopilot ? `- **@copilot** (Coding Agent) → label: \`squad:copilot\`` : '', + '', + `> To reassign, remove the current \`squad:*\` label and add the correct one.`, + ].filter(Boolean).join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment + }); + + core.info(`Triaged issue #${issue.number} → ${assignedMember.name} (${assignLabel})`); diff --git a/.squad/templates/workflows/sync-squad-labels.yml b/.squad/templates/workflows/sync-squad-labels.yml new file mode 100644 index 000000000..699fc680f --- /dev/null +++ b/.squad/templates/workflows/sync-squad-labels.yml @@ -0,0 +1,171 @@ +name: Sync Squad Labels + +on: + push: + paths: + - '.squad/team.md' + - '.ai-team/team.md' + workflow_dispatch: + +permissions: + issues: write + contents: read + +jobs: + sync-labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse roster and sync labels + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let teamFile = '.squad/team.md'; + if (!fs.existsSync(teamFile)) { + teamFile = '.ai-team/team.md'; + } + + if (!fs.existsSync(teamFile)) { + core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync'); + return; + } + + const content = fs.readFileSync(teamFile, 'utf8'); + const lines = content.split('\n'); + + // Parse the Members table for agent names + const members = []; + let inMembersTable = false; + for (const line of lines) { + if (line.match(/^##\s+(Members|Team Roster)/i)) { + inMembersTable = true; + continue; + } + if (inMembersTable && line.startsWith('## ')) { + break; + } + if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { + const cells = line.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && cells[0] !== 'Scribe') { + members.push({ + name: cells[0], + role: cells[1] + }); + } + } + } + + core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`); + + // Check if @copilot is on the team + const hasCopilot = content.includes('šŸ¤– Coding Agent'); + + // Define label color palette for squad labels + const SQUAD_COLOR = '9B8FCC'; + const MEMBER_COLOR = '9B8FCC'; + const COPILOT_COLOR = '10b981'; + + // Define go: and release: labels (static) + const GO_LABELS = [ + { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' }, + { name: 'go:no', color: 'B60205', description: 'Not pursuing' }, + { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' } + ]; + + const RELEASE_LABELS = [ + { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' }, + { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' }, + { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' }, + { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' }, + { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' } + ]; + + const TYPE_LABELS = [ + { name: 'type:feature', color: 'DDD1F2', description: 'New capability' }, + { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, + { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' }, + { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' }, + { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' }, + { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' } + ]; + + // High-signal labels — these MUST visually dominate all others + const SIGNAL_LABELS = [ + { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' }, + { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' } + ]; + + const PRIORITY_LABELS = [ + { name: 'priority:p0', color: 'B60205', description: 'Blocking release' }, + { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' }, + { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' } + ]; + + function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } + + // Ensure the base "squad" triage label exists + const labels = [ + { name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox — Lead will assign to a member' } + ]; + + for (const member of members) { + labels.push({ + name: `squad:${slugify(member.name)}`, + color: MEMBER_COLOR, + description: `Assigned to ${member.name} (${member.role})` + }); + } + + // Add @copilot label if coding agent is on the team + if (hasCopilot) { + labels.push({ + name: 'squad:copilot', + color: COPILOT_COLOR, + description: 'Assigned to @copilot (Coding Agent) for autonomous work' + }); + } + + // Add go:, release:, type:, priority:, and high-signal labels + labels.push(...GO_LABELS); + labels.push(...RELEASE_LABELS); + labels.push(...TYPE_LABELS); + labels.push(...PRIORITY_LABELS); + labels.push(...SIGNAL_LABELS); + + // Sync labels (create or update) + for (const label of labels) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name + }); + // Label exists — update it + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + core.info(`Updated label: ${label.name}`); + } catch (err) { + if (err.status === 404) { + // Label doesn't exist — create it + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + core.info(`Created label: ${label.name}`); + } else { + throw err; + } + } + } + + core.info(`Label sync complete: ${labels.length} labels synced`); diff --git a/.squad/templates/worktree-reference.md b/.squad/templates/worktree-reference.md new file mode 100644 index 000000000..958fee2e9 --- /dev/null +++ b/.squad/templates/worktree-reference.md @@ -0,0 +1,126 @@ +# Worktree Reference + +### Worktree Awareness + +Squad and all spawned agents may be running inside a **git worktree** rather than the main checkout. All `.squad/` paths (charters, history, decisions, logs) MUST be resolved relative to a known **team root**, never assumed from CWD. + +**Two strategies for resolving the team root:** + +| Strategy | Team root | State scope | When to use | +|----------|-----------|-------------|-------------| +| **worktree-local** | Current worktree root | Branch-local — each worktree has its own `.squad/` state | Feature branches that need isolated decisions and history | +| **main-checkout** | Main working tree root | Shared — all worktrees read/write the main checkout's `.squad/` | Single source of truth for memories, decisions, and logs across all branches | + +**How the Coordinator resolves the team root (on every session start):** + +0. **Check config.json overrides first** — read `.squad/config.json` in the current directory (or at the git root): + - If `teamRoot` is set → Team root = that path. **STOP — do not walk further.** + - If `stateLocation` is `"external"` → Resolve external AppData path. Team root = external path. **STOP.** + - Otherwise → continue to step 1. +1. **Check CWD first** — does `.squad/` exist in the current working directory? + - **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder. +2. If not, run `git rev-parse --show-toplevel` to get the current worktree root. +3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). + - **Yes** → use **worktree-local** strategy. Team root = current worktree root. + - **No** → use **main-checkout** strategy. Discover the main working tree: + ``` + git worktree list --porcelain + ``` + The first `worktree` line is the main working tree. Team root = that path. +4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). + +**Passing the team root to agents:** +- The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt. +- Agents resolve ALL `.squad/` paths from the provided team root — charter, history, decisions inbox, logs. +- Agents never discover the team root themselves. They trust the value from the Coordinator. + +**Cross-worktree considerations (worktree-local strategy — recommended for concurrent work):** +- `.squad/` files are **branch-local**. Each worktree works independently — no locking, no shared-state races. +- When branches merge into main, `.squad/` state merges with them. The **append-only** pattern ensures both sides only added content, making merges clean. +- A `merge=union` driver in `.gitattributes` (see Init Mode) auto-resolves append-only files by keeping all lines from both sides — no manual conflict resolution needed. +- The Scribe commits `.squad/` changes to the worktree's branch. State flows to other branches through normal git merge / PR workflow. + +**Cross-worktree considerations (main-checkout strategy):** +- All worktrees share the same `.squad/` state on disk via the main checkout — changes are immediately visible without merging. +- **Not safe for concurrent sessions.** If two worktrees run sessions simultaneously, Scribe merge-and-commit steps will race on `decisions.md` and git index. Use only when a single session is active at a time. +- Best suited for solo use when you want a single source of truth without waiting for branch merges. + +### Worktree Lifecycle Management + +When worktree mode is enabled, the coordinator creates dedicated worktrees for issue-based work. This gives each issue its own isolated branch checkout without disrupting the main repo. + +**Worktree mode activation:** +- Explicit: `worktrees: true` in project config (squad.config.ts or package.json `squad` section) +- Environment: `SQUAD_WORKTREES=1` set in environment variables +- Default: `false` (backward compatibility — agents work in the main repo) + +**Creating worktrees:** +- One worktree per issue number +- Multiple agents on the same issue share a worktree +- Path convention: `{repo-parent}/{repo-name}-{issue-number}` + - Example: Working on issue #42 in `C:\src\squad` → worktree at `C:\src\squad-42` +- Branch: `squad/{issue-number}-{kebab-case-slug}` (created from base branch, typically `main`) + +**Dependency management:** +- After creating a worktree, link `node_modules` from the main repo to avoid reinstalling +- Windows: `cmd /c "mklink /J {worktree}\node_modules {main-repo}\node_modules"` +- Unix: `ln -s {main-repo}/node_modules {worktree}/node_modules` +- If linking fails (permissions, cross-device), fall back to `npm install` in the worktree + +**Reusing worktrees:** +- Before creating a new worktree, check if one exists for the same issue +- `git worktree list` shows all active worktrees +- If found, reuse it (cd to the path, verify branch is correct, `git pull` to sync) +- Multiple agents can work in the same worktree concurrently if they modify different files + +**Cleanup:** +- After a PR is merged, the worktree should be removed +- `git worktree remove {path}` + `git branch -d {branch}` +- Ralph heartbeat can trigger cleanup checks for merged branches + +### Pre-Spawn: Worktree Setup + +When spawning an agent for issue-based work (user request references an issue number, or agent is working on a GitHub issue): + +**1. Check worktree mode:** +- Is `SQUAD_WORKTREES=1` set in the environment? +- Or does the project config have `worktrees: true`? +- If neither: skip worktree setup → agent works in the main repo (existing behavior) + +**2. If worktrees enabled:** + +a. **Determine the worktree path:** + - Parse issue number from context (e.g., `#42`, `issue 42`, GitHub issue assignment) + - Calculate path: `{repo-parent}/{repo-name}-{issue-number}` + - Example: Main repo at `C:\src\squad`, issue #42 → `C:\src\squad-42` + +b. **Check if worktree already exists:** + - Run `git worktree list` to see all active worktrees + - If the worktree path already exists → **reuse it**: + - Verify the branch is correct (should be `squad/{issue-number}-*`) + - `cd` to the worktree path + - `git pull` to sync latest changes + - Skip to step (e) + +c. **Create the worktree:** + - Determine branch name: `squad/{issue-number}-{kebab-case-slug}` (derive slug from issue title if available) + - Determine base branch (typically `main`, check default branch if needed) + - Run: `git worktree add {path} -b {branch} {baseBranch}` + - Example: `git worktree add C:\src\squad-42 -b squad/42-fix-login main` + +d. **Set up dependencies:** + - Link `node_modules` from main repo to avoid reinstalling: + - Windows: `cmd /c "mklink /J {worktree}\node_modules {main-repo}\node_modules"` + - Unix: `ln -s {main-repo}/node_modules {worktree}/node_modules` + - If linking fails (error), fall back: `cd {worktree} && npm install` + - Verify the worktree is ready: check build tools are accessible + +e. **Include worktree context in spawn:** + - Set `WORKTREE_PATH` to the resolved worktree path + - Set `WORKTREE_MODE` to `true` + - Add worktree instructions to the spawn prompt (see template below) + +**3. If worktrees disabled:** +- Set `WORKTREE_PATH` to `"n/a"` +- Set `WORKTREE_MODE` to `false` +- Use existing `git checkout -b` flow (no changes to current behavior)