@mmerterden/multi-agent-pipeline 12.7.0 → 12.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +207 -0
- package/install/_common.mjs +48 -0
- package/install/_dev-only-files.mjs +125 -5
- package/install/claude.mjs +14 -8
- package/install/copilot.mjs +5 -8
- package/package.json +17 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
- package/pipeline/lib/credential-store.sh +20 -0
- package/pipeline/multi-agent-refs/_account-picker.md +1 -1
- package/pipeline/multi-agent-refs/_dev-context.md +1 -1
- package/pipeline/multi-agent-refs/_repo-picker.md +1 -1
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +65 -0
- package/pipeline/multi-agent-refs/features/url-enrichment.md +93 -0
- package/pipeline/multi-agent-refs/phases/operations.md +28 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +3 -83
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -2
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +50 -62
- package/pipeline/schemas/prefs.schema.json +6 -0
- package/pipeline/scripts/_smoke-root.sh +61 -0
- package/pipeline/scripts/agent-guard.py +57 -3
- package/pipeline/scripts/audit-log.sh +25 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +21 -7
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +0 -25
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +0 -30
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +0 -20
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +0 -15
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +0 -12
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +0 -43
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +0 -35
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +0 -35
- package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +0 -12
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-2-plan.json +0 -42
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-review.json +0 -20
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-triage.json +0 -15
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/task.json +0 -12
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-2-plan.json +0 -40
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-review.json +0 -20
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-triage.json +0 -15
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/04-frontend-next-hydration/task.json +0 -12
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-2-plan.json +0 -42
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-review.json +0 -28
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-triage.json +0 -27
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/05-ios-security-keychain/task.json +0 -12
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-2-plan.json +0 -41
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-review.json +0 -12
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-triage.json +0 -6
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/06-android-refactor-usecase/task.json +0 -12
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-1-analysis.json +0 -29
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-2-plan.json +0 -42
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-review.json +0 -28
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-triage.json +0 -27
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/07-backend-node-idempotency/task.json +0 -12
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-1-analysis.json +0 -25
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-2-plan.json +0 -31
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-review.json +0 -12
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-triage.json +0 -18
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/metadata.json +0 -14
- package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/task.json +0 -12
- package/pipeline/eval/golden-tasks/README.md +0 -65
- package/pipeline/eval/intent-cases.json +0 -40
- package/pipeline/eval/run-metrics-fixture.json +0 -93
- package/pipeline/eval/triage/01-empty-findings/expected.json +0 -6
- package/pipeline/eval/triage/01-empty-findings/input.json +0 -5
- package/pipeline/eval/triage/01-empty-findings/notes.md +0 -7
- package/pipeline/eval/triage/02-real-blocker/expected.json +0 -15
- package/pipeline/eval/triage/02-real-blocker/input.json +0 -14
- package/pipeline/eval/triage/02-real-blocker/notes.md +0 -7
- package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +0 -18
- package/pipeline/eval/triage/03-out-of-scope-defer/input.json +0 -14
- package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +0 -10
- package/pipeline/eval/triage/04-false-positive-reject/expected.json +0 -18
- package/pipeline/eval/triage/04-false-positive-reject/input.json +0 -14
- package/pipeline/eval/triage/04-false-positive-reject/notes.md +0 -10
- package/pipeline/eval/triage/05-mixed-classification/expected.json +0 -43
- package/pipeline/eval/triage/05-mixed-classification/input.json +0 -38
- package/pipeline/eval/triage/05-mixed-classification/notes.md +0 -17
- package/pipeline/eval/triage/06-severity-mismatch/expected.json +0 -15
- package/pipeline/eval/triage/06-severity-mismatch/input.json +0 -14
- package/pipeline/eval/triage/06-severity-mismatch/notes.md +0 -9
- package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +0 -27
- package/pipeline/eval/triage/07-duplicate-reviewers/input.json +0 -22
- package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +0 -9
- package/pipeline/eval/triage/08-style-misclassified/expected.json +0 -18
- package/pipeline/eval/triage/08-style-misclassified/input.json +0 -14
- package/pipeline/eval/triage/08-style-misclassified/notes.md +0 -9
- package/pipeline/eval/triage/09-cascading-finding/expected.json +0 -23
- package/pipeline/eval/triage/09-cascading-finding/input.json +0 -22
- package/pipeline/eval/triage/09-cascading-finding/notes.md +0 -9
- package/pipeline/eval/triage/10-deferred-crossref/expected.json +0 -18
- package/pipeline/eval/triage/10-deferred-crossref/input.json +0 -14
- package/pipeline/eval/triage/10-deferred-crossref/notes.md +0 -9
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +0 -27
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +0 -22
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +0 -14
- package/pipeline/eval/triage/README.md +0 -54
- package/pipeline/scripts/benchmark-phase-0.sh +0 -128
- package/pipeline/scripts/check-md-links.mjs +0 -88
- package/pipeline/scripts/eval-golden-tasks-live.mjs +0 -302
- package/pipeline/scripts/eval-golden-tasks.mjs +0 -224
- package/pipeline/scripts/eval-intent.mjs +0 -107
- package/pipeline/scripts/eval-mine-corpus.mjs +0 -211
- package/pipeline/scripts/eval-triage.mjs +0 -171
- package/pipeline/scripts/fixtures/diff-risk-android.diff +0 -40
- package/pipeline/scripts/fixtures/diff-risk-ios.diff +0 -48
- package/pipeline/scripts/fixtures/diff-risk-test-removal.diff +0 -40
- package/pipeline/scripts/fixtures/install-layout.tsv +0 -19
- package/pipeline/scripts/fixtures/pack-expected-count.txt +0 -1
- package/pipeline/scripts/fixtures/test-gap-node.diff +0 -30
- package/pipeline/scripts/fixtures/test-gap-python.diff +0 -32
- package/pipeline/scripts/lint-mcp-refs.mjs +0 -218
- package/pipeline/scripts/lint-skills.mjs +0 -154
- package/pipeline/scripts/run-smokes.mjs +0 -130
- package/pipeline/scripts/scorecard.mjs +0 -258
- package/pipeline/scripts/smoke-add-detail.sh +0 -137
- package/pipeline/scripts/smoke-agent-guard.sh +0 -74
- package/pipeline/scripts/smoke-agent-log-cost.sh +0 -262
- package/pipeline/scripts/smoke-agent-model-routing.sh +0 -87
- package/pipeline/scripts/smoke-ask-choice.sh +0 -42
- package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +0 -36
- package/pipeline/scripts/smoke-bitbucket-contract.sh +0 -255
- package/pipeline/scripts/smoke-changelog-version.sh +0 -47
- package/pipeline/scripts/smoke-channels-approval-gate.sh +0 -60
- package/pipeline/scripts/smoke-channels-flow.sh +0 -130
- package/pipeline/scripts/smoke-ci-workflows.sh +0 -88
- package/pipeline/scripts/smoke-clarify.sh +0 -148
- package/pipeline/scripts/smoke-command-inventory.sh +0 -81
- package/pipeline/scripts/smoke-commands-skills-parity.sh +0 -87
- package/pipeline/scripts/smoke-community-gates.sh +0 -75
- package/pipeline/scripts/smoke-compliance-skills.sh +0 -119
- package/pipeline/scripts/smoke-config-hygiene.sh +0 -58
- package/pipeline/scripts/smoke-context-budget.sh +0 -72
- package/pipeline/scripts/smoke-cost-budget.sh +0 -70
- package/pipeline/scripts/smoke-cost-summary.sh +0 -139
- package/pipeline/scripts/smoke-cross-phase-cohesion.sh +0 -128
- package/pipeline/scripts/smoke-description-tr.sh +0 -82
- package/pipeline/scripts/smoke-dev-critic.sh +0 -144
- package/pipeline/scripts/smoke-diff-explain.sh +0 -147
- package/pipeline/scripts/smoke-diff-risk.sh +0 -190
- package/pipeline/scripts/smoke-dynamic-skill-loading.sh +0 -160
- package/pipeline/scripts/smoke-eval-live.sh +0 -136
- package/pipeline/scripts/smoke-evidence-gate.sh +0 -93
- package/pipeline/scripts/smoke-extract-conventions.sh +0 -163
- package/pipeline/scripts/smoke-fetchers-offline.sh +0 -448
- package/pipeline/scripts/smoke-figma-dispatch.sh +0 -112
- package/pipeline/scripts/smoke-gate-hooks.sh +0 -74
- package/pipeline/scripts/smoke-gc-tmp.sh +0 -130
- package/pipeline/scripts/smoke-gc-worktrees.sh +0 -125
- package/pipeline/scripts/smoke-generate-issue.sh +0 -120
- package/pipeline/scripts/smoke-handoff-contract.sh +0 -92
- package/pipeline/scripts/smoke-identity-isolation.sh +0 -70
- package/pipeline/scripts/smoke-install-layout.sh +0 -248
- package/pipeline/scripts/smoke-intent-guard.sh +0 -86
- package/pipeline/scripts/smoke-issue-comment-template.sh +0 -86
- package/pipeline/scripts/smoke-issue-jira-triad.sh +0 -120
- package/pipeline/scripts/smoke-keychain.sh +0 -158
- package/pipeline/scripts/smoke-language-axis.sh +0 -109
- package/pipeline/scripts/smoke-learning-curve.sh +0 -61
- package/pipeline/scripts/smoke-learnings-ledger.sh +0 -86
- package/pipeline/scripts/smoke-lib-scripts.sh +0 -448
- package/pipeline/scripts/smoke-mcp-gate.sh +0 -68
- package/pipeline/scripts/smoke-md-links.sh +0 -8
- package/pipeline/scripts/smoke-md2confluence.sh +0 -126
- package/pipeline/scripts/smoke-metrics-cache-ratio.sh +0 -72
- package/pipeline/scripts/smoke-migrate-state.sh +0 -102
- package/pipeline/scripts/smoke-mode-dispatch-drift.sh +0 -161
- package/pipeline/scripts/smoke-model-fallback.sh +0 -89
- package/pipeline/scripts/smoke-multi-repo-integration.sh +0 -116
- package/pipeline/scripts/smoke-multi-repo-worktree.sh +0 -61
- package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +0 -194
- package/pipeline/scripts/smoke-no-token-prompt.sh +0 -85
- package/pipeline/scripts/smoke-own-punctuation.sh +0 -103
- package/pipeline/scripts/smoke-pack-contents.sh +0 -140
- package/pipeline/scripts/smoke-pat-audit.sh +0 -128
- package/pipeline/scripts/smoke-per-repo-memory.sh +0 -156
- package/pipeline/scripts/smoke-phase-0-multi-repo.sh +0 -170
- package/pipeline/scripts/smoke-phase-6-multi.sh +0 -79
- package/pipeline/scripts/smoke-phase-banner.sh +0 -101
- package/pipeline/scripts/smoke-phase-tracker.sh +0 -324
- package/pipeline/scripts/smoke-phase0-bridge-contract.sh +0 -241
- package/pipeline/scripts/smoke-phase4-gates.sh +0 -45
- package/pipeline/scripts/smoke-phase4-triage.sh +0 -229
- package/pipeline/scripts/smoke-plan-approval-gate.sh +0 -71
- package/pipeline/scripts/smoke-plan-safety.sh +0 -139
- package/pipeline/scripts/smoke-plan-todos.sh +0 -196
- package/pipeline/scripts/smoke-pr-review-actions.sh +0 -152
- package/pipeline/scripts/smoke-pre-commit.sh +0 -170
- package/pipeline/scripts/smoke-pref-migration.sh +0 -226
- package/pipeline/scripts/smoke-prefs-language.sh +0 -134
- package/pipeline/scripts/smoke-progress-contract.sh +0 -127
- package/pipeline/scripts/smoke-prune-logs.sh +0 -137
- package/pipeline/scripts/smoke-purge.sh +0 -138
- package/pipeline/scripts/smoke-push-retry.sh +0 -75
- package/pipeline/scripts/smoke-repo-map.sh +0 -300
- package/pipeline/scripts/smoke-review-readiness.sh +0 -92
- package/pipeline/scripts/smoke-review-watch.sh +0 -146
- package/pipeline/scripts/smoke-routines.sh +0 -84
- package/pipeline/scripts/smoke-run-aggregator.sh +0 -216
- package/pipeline/scripts/smoke-run-metrics.sh +0 -50
- package/pipeline/scripts/smoke-search.sh +0 -187
- package/pipeline/scripts/smoke-shadow-git.sh +0 -224
- package/pipeline/scripts/smoke-skill-authoring.sh +0 -137
- package/pipeline/scripts/smoke-skill-language.sh +0 -83
- package/pipeline/scripts/smoke-skill-manifest.sh +0 -138
- package/pipeline/scripts/smoke-skill-scan.sh +0 -198
- package/pipeline/scripts/smoke-source-parity.sh +0 -85
- package/pipeline/scripts/smoke-subagent-validators.sh +0 -108
- package/pipeline/scripts/smoke-sync-parity.sh +0 -92
- package/pipeline/scripts/smoke-tasklist-ordering.sh +0 -112
- package/pipeline/scripts/smoke-telemetry.sh +0 -147
- package/pipeline/scripts/smoke-test-gap.sh +0 -183
- package/pipeline/scripts/smoke-token-budget.sh +0 -67
- package/pipeline/scripts/smoke-token-preflight.sh +0 -82
- package/pipeline/scripts/smoke-tracker-contract.sh +0 -191
- package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +0 -73
- package/pipeline/scripts/smoke-triage-memory.sh +0 -174
- package/pipeline/scripts/smoke-update-check.sh +0 -135
- package/pipeline/scripts/smoke-url-enrichment.sh +0 -70
- package/pipeline/scripts/smoke-validate-analysis-doc.sh +0 -161
- package/pipeline/scripts/smoke-validator-contradiction.sh +0 -67
- package/pipeline/scripts/smoke-validator-gates.sh +0 -164
- package/pipeline/scripts/smoke-vercel-deploy-redact.sh +0 -129
- package/pipeline/scripts/smoke-verify-by-test.sh +0 -148
- package/pipeline/scripts/smoke-wiki-integration.sh +0 -122
- package/pipeline/scripts/smoke-work-summary.sh +0 -163
- package/pipeline/scripts/smoke-workflow-audit.sh +0 -101
- package/pipeline/scripts/smoke-worktree-path-convention.sh +0 -86
- package/pipeline/scripts/smoke-wrapper-preservation.sh +0 -68
- package/pipeline/scripts/smoke-write-state.sh +0 -159
- package/pipeline/scripts/sync-parity-check.sh +0 -135
- package/pipeline/scripts/test-gap-rules/android.json +0 -25
- package/pipeline/scripts/test-gap-rules/ios.json +0 -34
- package/pipeline/scripts/test-gap-rules/node.json +0 -29
- package/pipeline/scripts/test-gap-rules/python.json +0 -25
- package/pipeline/scripts/validate-schemas.mjs +0 -88
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Phase 4 Review - Multi-Repo Mode
|
|
2
|
+
|
|
3
|
+
> Loaded on demand from `phases/phase-4-review.md`. Conditional on
|
|
4
|
+
> `state.projects[].length > 1`; a single-repo run (the common case) never needs
|
|
5
|
+
> it and used to pay for it on every review.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Active when `state.projects[].length > 1`. The 3-reviewer/triage architecture is unchanged - what changes is the diff that reviewers see.
|
|
9
|
+
|
|
10
|
+
**Combined diff assembly** - reviewers get **one combined diff** covering all repos, with explicit per-repo headers so they can flag cross-repo impact:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
COMBINED_DIFF=$(mktemp)
|
|
14
|
+
for proj in $(jq -r '.projects[] | "\(.name)\t\(.worktreePath)\t\(.baseBranch)"' "$STATE_FILE"); do
|
|
15
|
+
IFS=$'\t' read -r name wt base <<< "$proj"
|
|
16
|
+
ORIGIN=$(git -C "$wt" config --get remote.origin.url)
|
|
17
|
+
printf '\n=== repo: %s (origin: %s) ===\n' "$name" "$ORIGIN" >> "$COMBINED_DIFF"
|
|
18
|
+
printf 'Base: %s ... HEAD\n\n' "$base" >> "$COMBINED_DIFF"
|
|
19
|
+
git -C "$wt" diff "origin/$base...HEAD" >> "$COMBINED_DIFF"
|
|
20
|
+
done
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Same reviewer set (Fable-or-Opus / GPT-5.4 / Sonnet) receive `COMBINED_DIFF` with a multi-repo prefix in the system prompt:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
This is a multi-repo task spanning {N} repos: {repo names}.
|
|
27
|
+
Each repo's diff is prefixed with `=== repo: <name> ===`.
|
|
28
|
+
A finding may span repos (e.g. "common adds API X, but uicomponents calls X with wrong type").
|
|
29
|
+
For cross-repo findings, set `crossRepo: true` and list all impacted repos in `affectedRepos: []`.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Schema extension for findings** (additive, single-repo `crossRepo` defaults to false):
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"severity": "blocking|important|suggestion",
|
|
37
|
+
"file": "uicomponents/Sources/Foo.swift",
|
|
38
|
+
"line": 42,
|
|
39
|
+
"issue": "...",
|
|
40
|
+
"fix": "...",
|
|
41
|
+
"crossRepo": false,
|
|
42
|
+
"affectedRepos": []
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`file` paths are repo-name-prefixed (`<repo-name>/<path-relative-to-repo>`) so triage and rework can resolve them back to the right worktree. The triage prompt receives `state.projects[]` so it can validate `affectedRepos` entries.
|
|
47
|
+
|
|
48
|
+
**Triage cross-check**: Cross-repo findings are NEVER auto-rejected - the cross-cutting nature is exactly what reviewers are best at catching. Triage may still defer (out-of-scope) but rejection requires explicit reasoning.
|
|
49
|
+
|
|
50
|
+
**Token-budget guard** (per `token-budget.json` Phase 4 allowance): if `COMBINED_DIFF` exceeds 80% of the budget, truncate the largest repo's diff with a footer header:
|
|
51
|
+
```
|
|
52
|
+
[truncated - full diff in file://$WORKTREE/.review-diff.txt]
|
|
53
|
+
```
|
|
54
|
+
And log `review.diff_truncated repo=<name> bytes_dropped=<N>`. Triage receives the same truncated view + the truncation marker so it can flag suggestions to "review the full diff manually."
|
|
55
|
+
|
|
56
|
+
**Deterministic gates per repo**: Step 1 gates (build/lint/test/secrets) run **per repo** - failure in any one repo blocks AI review for the whole task. Build queue lock applies as in Phase 3 (multi-repo doesn't change Xcode serialization).
|
|
57
|
+
|
|
58
|
+
**Telemetry**: Per-repo build/test gate timings + a single combined review/triage call set:
|
|
59
|
+
```bash
|
|
60
|
+
pipeline/scripts/log-metric.sh "$TASK_ID" 4 gate.build repo=common status=pass duration_ms=$D
|
|
61
|
+
pipeline/scripts/log-metric.sh "$TASK_ID" 4 gate.build repo=uicomponents status=pass duration_ms=$D
|
|
62
|
+
pipeline/scripts/log-metric.sh "$TASK_ID" 4 review.combined_diff repos=2 bytes=$BYTES truncated=false
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Step 1b - URL Enrichment (Phase 0)
|
|
2
|
+
|
|
3
|
+
> Loaded on demand from `phases/phase-0-init.md` Step 1b. It lives here rather
|
|
4
|
+
> than inline because it applies only to a task that carries URLs: a bare Jira
|
|
5
|
+
> ID, a GitHub issue number, or a free-text task never needs any of it, yet every
|
|
6
|
+
> run used to pay for all 9.6 kB of it. Deep-fetch sub-steps 1b.1-1b.3 are
|
|
7
|
+
> themselves conditional on `state.contextLinks[]` carrying that link type.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
After Step 1 fetches the Jira description (or free-text input), every URL in the text gets classified and catalogued. Targeted deep fetches (Crashlytics, Fortify) run when the matching link type is present; the rest (Swagger, Confluence, Figma, generic doc) are catalogued so Phase 1 (Analysis) can consume them via the typed fetchers documented in `$HOME/.claude/multi-agent-refs/channels/*.md`.
|
|
11
|
+
|
|
12
|
+
##### Step 1b.0 - Extract context links (always runs)
|
|
13
|
+
|
|
14
|
+
Pipe the description text through `~/.claude/lib/context-link-extractor.sh` and store the result on the state:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
printf '%s' "$DESCRIPTION_TEXT" \
|
|
18
|
+
| ~/.claude/lib/context-link-extractor.sh \
|
|
19
|
+
> /tmp/context-links-${TASK_ID}.json
|
|
20
|
+
|
|
21
|
+
# Persist into agent-state.json under state.contextLinks
|
|
22
|
+
jq --slurpfile links /tmp/context-links-${TASK_ID}.json \
|
|
23
|
+
'.contextLinks = $links[0]' \
|
|
24
|
+
"$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Output schema (one array entry per URL - full contract in `~/.claude/lib/context-link-extractor.sh`):
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
"contextLinks": [
|
|
31
|
+
{ "type": "swagger", "url": "<url>", "metadata": { "format": "json|yaml|unknown" } },
|
|
32
|
+
{ "type": "confluence", "url": "<url>", "metadata": { "host": "<host>", "pageId": "<id|null>", "spaceKey": "<key|null>" } },
|
|
33
|
+
{ "type": "crashlytics", "url": "<url>", "metadata": { "projectId": "...", "platform": "ios|android", "bundle": "...", "issueId": "...", "sessionId": "<id|null>" } },
|
|
34
|
+
{ "type": "fortify", "url": "<url>", "metadata": { "host": "<host>", "projectId": "<id|null>" } },
|
|
35
|
+
{ "type": "graylog", "url": null, "metadata": { "idType": "trx|conversation", "id": "...", "label": "..." } },
|
|
36
|
+
{ "type": "figma", "url": "<url>", "metadata": { "kind": "design|make|board|slides", "fileKey": "...", "nodeId": "<id|null>" } },
|
|
37
|
+
{ "type": "generic-doc", "url": "<url>", "metadata": { "host": "<host>", "kind": "notion|google-docs|gist|github-doc" } }
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The catalogue is the single source of truth for downstream phases - they read `state.contextLinks[]` and dispatch per `type`. The extractor is deterministic: same input → same output, no network calls, no auth.
|
|
42
|
+
|
|
43
|
+
##### Step 1b.1 - Firebase Crashlytics deep fetch (runs when `type == "crashlytics"` present in `state.contextLinks[]`)
|
|
44
|
+
|
|
45
|
+
URL pattern (caught by the extractor): `console.firebase.google.com/(u/[0-9]+/)?project/<projectId>/crashlytics/app/(ios|android)(:|%3A)<bundleOrPackage>/issues/<issueId>(/sessions/<sessionId>)?`
|
|
46
|
+
|
|
47
|
+
- Host is fixed (`console.firebase.google.com`); no `hosts.firebase` pref needed.
|
|
48
|
+
- Token resolution: `prefs.global.keychainMapping.firebase` → `~/.claude/lib/credential-store.sh get "<key>" | base64 -d` → Service Account JSON. `project_id` read from decoded JSON - verify it matches the `<projectId>` from the URL; mismatch → warn, skip enrichment.
|
|
49
|
+
- Exchange the SA JSON for a short-lived GCP access token (scope `https://www.googleapis.com/auth/firebase https://www.googleapis.com/auth/cloud-platform`) using `google-auth` (Python) or an inline JWT exchange (OpenSSL sign → `https://oauth2.googleapis.com/token`). Cache in-memory for the pipeline run only - never persist.
|
|
50
|
+
- Fetch crash detail: `GET https://firebasecrashlytics.googleapis.com/v1alpha/projects/<projectId>/apps/<appId>/issues/<issueId>` (issue summary + top device + OS + impacted users + top stack frame). If `sessionId` present, also fetch `/sessions/<sessionId>` for the full session timeline.
|
|
51
|
+
- Store as `state.crashContext = { projectId, appId, issueId, title, subtitle, impactedUsers, topDevices[], topOS[], topStackFrame, sessionTimeline?, fetchedAt }`.
|
|
52
|
+
- Phase 1 Analysis prepends `state.crashContext` to the agent prompt under a **Known Crash Context** section (do not rediscover, use as ground truth).
|
|
53
|
+
- Soft-fail: any network/auth error → log warning, continue pipeline without enrichment (ticket still gets manually analysed).
|
|
54
|
+
|
|
55
|
+
##### Step 1b.2 - Fortify SSC deep fetch (runs when `type == "fortify"` present in `state.contextLinks[]`)
|
|
56
|
+
|
|
57
|
+
URL pattern (caught by the extractor): `https://<prefs.global.hosts.fortify>/ssc/html/(ssc/)?version/<versionId>(/.*)?(#/version/<versionId>)?(/audit|/issues|/artifacts)?(\?issue=<issueId>)?` (plus the short-link variant `/ssc/html/ssc/version/<versionId>/fix/<issueInstanceId>`).
|
|
58
|
+
|
|
59
|
+
- Host comes from `prefs.global.hosts.fortify` (collected during Token Save Flow Step 3.5). URL host must match, else warn and skip (protects against pasting another company's Fortify URL by accident).
|
|
60
|
+
- Token resolution: `prefs.global.keychainMapping.fortify` → `~/.claude/lib/credential-store.sh get "<key>"` → API token. Auth header: `Authorization: FortifyToken <base64(token)>` (legacy tokens) or `Bearer <token>` (scoped tokens) - try Bearer first, fall back to FortifyToken on 401.
|
|
61
|
+
- Fetch finding detail (VPN required; respect `vpnServices.fortify`):
|
|
62
|
+
- Version: `GET /ssc/api/v1/projectVersions/<versionId>` → name, project name, current issue count.
|
|
63
|
+
- Issue list / single issue: if `issueInstanceId` in URL → `GET /ssc/api/v1/projectVersions/<versionId>/issues?q=issueInstanceId:"<id>"&fields=id,issueName,friority,fullFileName,lineNumber,analyzer,kingdom`. Otherwise fetch top open issue of the version.
|
|
64
|
+
- Issue details + recommendation: `GET /ssc/api/v1/issueDetails/<issueId>` → description, recommendation, code snippet (if the artifact is retained).
|
|
65
|
+
- Store as `state.fortifyFinding = { host, versionId, versionName, projectName, issueId?, issueName?, severity, friority, kingdom, analyzer, file, line, description, recommendation, codeSnippet?, fetchedAt }`.
|
|
66
|
+
- Phase 2 Planning injects `state.fortifyFinding` into the architectural-review prompt under a **Known Security Finding** section, and the generated task breakdown includes a task addressing the finding (naming convention: `sec(fortify-<issueId>): <issueName> at <file>:<line>`).
|
|
67
|
+
- Soft-fail: 401 → re-run Token Save Flow for `fortify`; 403 → warn and skip; network timeout → respect VPN fallback (skip enrichment, mark `state.fortifyFinding = { skipped: "vpn-unreachable" }`).
|
|
68
|
+
|
|
69
|
+
##### Step 1b.3 - Graylog deep fetch (runs when `type == "graylog"` present in `state.contextLinks[]`)
|
|
70
|
+
|
|
71
|
+
The extractor emits `graylog` entries from free-text trx / conversation ids (labels like `trx`, `trxId`, `transaction id`, `conversationId`, `convId`, `X-conversationId`), not from a URL - so these entries carry `"url": null`. When at least one is present, pull the matching diagnostic logs.
|
|
72
|
+
|
|
73
|
+
- Host comes from `prefs.global.hosts.graylog` (`GRAYLOG_HOST_OVERRIDE` env forces it for tests/CI); no literal host lives in any source file. Missing host → the fetcher exits `6`; log a setup hint and skip.
|
|
74
|
+
- Token resolution: `prefs.global.keychainMapping.graylog` → `~/.claude/lib/credential-store.sh get "<key>"` → Graylog PAT (fallback key `${USER}_Graylog_Access_Token`). Auth is HTTP Basic with the token as username and the literal `token` as password; the token is fed to `curl` only via a `-K` config file (process substitution), never on argv.
|
|
75
|
+
- Fetch logs (VPN required; respect `vpnServices.graylog`): call `~/.claude/lib/fetch-graylog.sh --trx <id>` (or `--conv <id>` for conversation ids) once per extracted id. The fetcher emits a normalized JSON object of matching log messages.
|
|
76
|
+
- Store as `state.graylogContext = { idType, id, messages[], fetchedAt }` (or an array keyed per id when several were extracted).
|
|
77
|
+
- Phase 1 Analysis prepends `state.graylogContext` to the agent prompt under the **Referenced External Sources** section as **diagnostic context, advisory only** (never contradicts code; no task is generated from it, no Phase 4 gate).
|
|
78
|
+
- Soft-fail (non-blocking, mirrors Crashlytics): unreachable host / VPN down / network timeout → the fetcher emits a normalized empty object and exits `0`; mark `state.graylogContext = { skipped: "vpn-unreachable" }` and continue. Only a genuine auth rejection (4xx on a reachable host, fetcher exit `3`) is logged as `skipped` with the auth reason. A log fetch never blocks the run.
|
|
79
|
+
|
|
80
|
+
##### Step 1b.4 - Other link types (catalogued only at Phase 0; fetched at Phase 1)
|
|
81
|
+
|
|
82
|
+
For `swagger`, `confluence`, `figma`, `generic-doc` entries in `state.contextLinks[]`, no Phase 0 fetch runs. The entries pass through to Phase 1, which dispatches the matching fetcher (Block B work: `fetch-swagger.sh`, `fetch-confluence.sh`, etc.). Until those fetchers land, the entries are surfaced in Phase 1 context as "referenced but not fetched - agent should treat the URL as an authoritative external source and read it directly when relevant."
|
|
83
|
+
|
|
84
|
+
**Jira project key addendum**: Support multi-tenant Jira - `prefs.projects[{project}].jiraProjectKeys[]` is already a list. When Step 1 extracts a Jira key from input, `upsert` it at the head (dedup, cap 10). Sibling keys for the same project (e.g. one key per team or tracker board) coexist; `prefs.global.defaultJiraKey` is the fallback for ambiguous free-text input only.
|
|
85
|
+
|
|
86
|
+
**Log line shape** (progress contract):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
→ context links: total=<N>, by-type={swagger:<n>, confluence:<n>, crashlytics:<n>, fortify:<n>, graylog:<n>, figma:<n>, generic-doc:<n>}
|
|
90
|
+
→ URL deep fetch: crashlytics=<hit|miss|skipped>, fortify=<hit|miss|skipped>, graylog=<hit|miss|skipped>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Two lines emit at the end of Step 1b. The first reports the catalogue from 1b.0 (always runs). The second reports the deep-fetch outcomes from 1b.1 / 1b.2 / 1b.3 - `miss` = type not present in catalogue (normal), `skipped` = present but auth/VPN/mismatch skipped the fetch (for graylog this includes the non-blocking VPN-unreachable degrade), `hit` = context stored in state.
|
|
@@ -67,6 +67,34 @@ Run phases sequentially. Log every step to `agent-log.md`.
|
|
|
67
67
|
If any phase fails, mark task as `paused` and stop - user can `resume` later.
|
|
68
68
|
Update `agent-state.json` at EVERY phase transition.
|
|
69
69
|
|
|
70
|
+
### Writing `agent-state.json` (required mechanism)
|
|
71
|
+
|
|
72
|
+
Every state update goes through `write-state.mjs`. Never write the file with a
|
|
73
|
+
plain read-modify-write (`jq ... > tmp && mv`, an editor tool, `cat >`):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Merge a patch into the current state (the normal case).
|
|
77
|
+
echo '{"currentPhase":4,"phases":{"4":{"status":"running"}}}' \
|
|
78
|
+
| node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE"
|
|
79
|
+
|
|
80
|
+
# Full replacement, only when rewriting the document wholesale.
|
|
81
|
+
printf '%s' "$NEW_STATE" | node $HOME/.claude/scripts/write-state.mjs --replace "$STATE_FILE"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Why the indirection is required: multi-repo mode and concurrent worktrees share
|
|
85
|
+
one `agent-state.json` per task id, so two writers using read-then-write lose one
|
|
86
|
+
of the two updates - the second write is computed from a snapshot taken before
|
|
87
|
+
the first landed. `write-state.mjs` does tmpfile + rename (atomic on POSIX) under
|
|
88
|
+
an advisory `.lock`, reclaims a lock whose holder PID is dead, and releases the
|
|
89
|
+
lock on every error path.
|
|
90
|
+
|
|
91
|
+
Exit codes the caller must handle: `0` written, `1` invalid JSON on stdin, `2`
|
|
92
|
+
lock timeout (another writer held it past the acquire window - retry once, then
|
|
93
|
+
halt per the halt-visibility rule), `3` I/O error.
|
|
94
|
+
|
|
95
|
+
Reads need no wrapper; the rename makes any read see either the old or the new
|
|
96
|
+
document, never a truncated one.
|
|
97
|
+
|
|
70
98
|
**Halt visibility (required, autopilot included).** A halt is never silent. Whenever a phase halts on a hard error (validator failed twice, no subagent returned, dispatch error past fallback, lock irrecoverable), in addition to the `agent-log.md` line: (a) write `state.status = "paused"` and `state.haltReason = "<phase>:<cause>"`; (b) record the cause on the tracker via `phase-tracker.sh meta <phase> halt "<cause>"` and `phase-tracker.sh update <phase> failed`; (c) emit one `>&2` alert line `HALT phase <N>: <cause> - resume with /multi-agent:resume #<id>`. Autopilot suppresses *confirmations*, not *halts* - the user must always be able to see why an unattended run stopped without reading the log.
|
|
71
99
|
|
|
72
100
|
### Pipeline Best Practices
|
|
@@ -200,90 +200,9 @@ Sequential prompts (standard UX pattern with Recent suggestion): Project Key →
|
|
|
200
200
|
|
|
201
201
|
#### Step 1b - URL Enrichment (catalogue + targeted deep fetches)
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
**Runs only when Step 1 found at least one URL in the task input** (Jira/Confluence/Figma/Swagger/Crashlytics/Fortify/Graylog links). Otherwise skip straight to Step 2.
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
Pipe the description text through `~/.claude/lib/context-link-extractor.sh` and store the result on the state:
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
printf '%s' "$DESCRIPTION_TEXT" \
|
|
211
|
-
| ~/.claude/lib/context-link-extractor.sh \
|
|
212
|
-
> /tmp/context-links-${TASK_ID}.json
|
|
213
|
-
|
|
214
|
-
# Persist into agent-state.json under state.contextLinks
|
|
215
|
-
jq --slurpfile links /tmp/context-links-${TASK_ID}.json \
|
|
216
|
-
'.contextLinks = $links[0]' \
|
|
217
|
-
"$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Output schema (one array entry per URL - full contract in `~/.claude/lib/context-link-extractor.sh`):
|
|
221
|
-
|
|
222
|
-
```jsonc
|
|
223
|
-
"contextLinks": [
|
|
224
|
-
{ "type": "swagger", "url": "<url>", "metadata": { "format": "json|yaml|unknown" } },
|
|
225
|
-
{ "type": "confluence", "url": "<url>", "metadata": { "host": "<host>", "pageId": "<id|null>", "spaceKey": "<key|null>" } },
|
|
226
|
-
{ "type": "crashlytics", "url": "<url>", "metadata": { "projectId": "...", "platform": "ios|android", "bundle": "...", "issueId": "...", "sessionId": "<id|null>" } },
|
|
227
|
-
{ "type": "fortify", "url": "<url>", "metadata": { "host": "<host>", "projectId": "<id|null>" } },
|
|
228
|
-
{ "type": "graylog", "url": null, "metadata": { "idType": "trx|conversation", "id": "...", "label": "..." } },
|
|
229
|
-
{ "type": "figma", "url": "<url>", "metadata": { "kind": "design|make|board|slides", "fileKey": "...", "nodeId": "<id|null>" } },
|
|
230
|
-
{ "type": "generic-doc", "url": "<url>", "metadata": { "host": "<host>", "kind": "notion|google-docs|gist|github-doc" } }
|
|
231
|
-
]
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
The catalogue is the single source of truth for downstream phases - they read `state.contextLinks[]` and dispatch per `type`. The extractor is deterministic: same input → same output, no network calls, no auth.
|
|
235
|
-
|
|
236
|
-
##### Step 1b.1 - Firebase Crashlytics deep fetch (runs when `type == "crashlytics"` present in `state.contextLinks[]`)
|
|
237
|
-
|
|
238
|
-
URL pattern (caught by the extractor): `console.firebase.google.com/(u/[0-9]+/)?project/<projectId>/crashlytics/app/(ios|android)(:|%3A)<bundleOrPackage>/issues/<issueId>(/sessions/<sessionId>)?`
|
|
239
|
-
|
|
240
|
-
- Host is fixed (`console.firebase.google.com`); no `hosts.firebase` pref needed.
|
|
241
|
-
- Token resolution: `prefs.global.keychainMapping.firebase` → `~/.claude/lib/credential-store.sh get "<key>" | base64 -d` → Service Account JSON. `project_id` read from decoded JSON - verify it matches the `<projectId>` from the URL; mismatch → warn, skip enrichment.
|
|
242
|
-
- Exchange the SA JSON for a short-lived GCP access token (scope `https://www.googleapis.com/auth/firebase https://www.googleapis.com/auth/cloud-platform`) using `google-auth` (Python) or an inline JWT exchange (OpenSSL sign → `https://oauth2.googleapis.com/token`). Cache in-memory for the pipeline run only - never persist.
|
|
243
|
-
- Fetch crash detail: `GET https://firebasecrashlytics.googleapis.com/v1alpha/projects/<projectId>/apps/<appId>/issues/<issueId>` (issue summary + top device + OS + impacted users + top stack frame). If `sessionId` present, also fetch `/sessions/<sessionId>` for the full session timeline.
|
|
244
|
-
- Store as `state.crashContext = { projectId, appId, issueId, title, subtitle, impactedUsers, topDevices[], topOS[], topStackFrame, sessionTimeline?, fetchedAt }`.
|
|
245
|
-
- Phase 1 Analysis prepends `state.crashContext` to the agent prompt under a **Known Crash Context** section (do not rediscover, use as ground truth).
|
|
246
|
-
- Soft-fail: any network/auth error → log warning, continue pipeline without enrichment (ticket still gets manually analysed).
|
|
247
|
-
|
|
248
|
-
##### Step 1b.2 - Fortify SSC deep fetch (runs when `type == "fortify"` present in `state.contextLinks[]`)
|
|
249
|
-
|
|
250
|
-
URL pattern (caught by the extractor): `https://<prefs.global.hosts.fortify>/ssc/html/(ssc/)?version/<versionId>(/.*)?(#/version/<versionId>)?(/audit|/issues|/artifacts)?(\?issue=<issueId>)?` (plus the short-link variant `/ssc/html/ssc/version/<versionId>/fix/<issueInstanceId>`).
|
|
251
|
-
|
|
252
|
-
- Host comes from `prefs.global.hosts.fortify` (collected during Token Save Flow Step 3.5). URL host must match, else warn and skip (protects against pasting another company's Fortify URL by accident).
|
|
253
|
-
- Token resolution: `prefs.global.keychainMapping.fortify` → `~/.claude/lib/credential-store.sh get "<key>"` → API token. Auth header: `Authorization: FortifyToken <base64(token)>` (legacy tokens) or `Bearer <token>` (scoped tokens) - try Bearer first, fall back to FortifyToken on 401.
|
|
254
|
-
- Fetch finding detail (VPN required; respect `vpnServices.fortify`):
|
|
255
|
-
- Version: `GET /ssc/api/v1/projectVersions/<versionId>` → name, project name, current issue count.
|
|
256
|
-
- Issue list / single issue: if `issueInstanceId` in URL → `GET /ssc/api/v1/projectVersions/<versionId>/issues?q=issueInstanceId:"<id>"&fields=id,issueName,friority,fullFileName,lineNumber,analyzer,kingdom`. Otherwise fetch top open issue of the version.
|
|
257
|
-
- Issue details + recommendation: `GET /ssc/api/v1/issueDetails/<issueId>` → description, recommendation, code snippet (if the artifact is retained).
|
|
258
|
-
- Store as `state.fortifyFinding = { host, versionId, versionName, projectName, issueId?, issueName?, severity, friority, kingdom, analyzer, file, line, description, recommendation, codeSnippet?, fetchedAt }`.
|
|
259
|
-
- Phase 2 Planning injects `state.fortifyFinding` into the architectural-review prompt under a **Known Security Finding** section, and the generated task breakdown includes a task addressing the finding (naming convention: `sec(fortify-<issueId>): <issueName> at <file>:<line>`).
|
|
260
|
-
- Soft-fail: 401 → re-run Token Save Flow for `fortify`; 403 → warn and skip; network timeout → respect VPN fallback (skip enrichment, mark `state.fortifyFinding = { skipped: "vpn-unreachable" }`).
|
|
261
|
-
|
|
262
|
-
##### Step 1b.3 - Graylog deep fetch (runs when `type == "graylog"` present in `state.contextLinks[]`)
|
|
263
|
-
|
|
264
|
-
The extractor emits `graylog` entries from free-text trx / conversation ids (labels like `trx`, `trxId`, `transaction id`, `conversationId`, `convId`, `X-conversationId`), not from a URL - so these entries carry `"url": null`. When at least one is present, pull the matching diagnostic logs.
|
|
265
|
-
|
|
266
|
-
- Host comes from `prefs.global.hosts.graylog` (`GRAYLOG_HOST_OVERRIDE` env forces it for tests/CI); no literal host lives in any source file. Missing host → the fetcher exits `6`; log a setup hint and skip.
|
|
267
|
-
- Token resolution: `prefs.global.keychainMapping.graylog` → `~/.claude/lib/credential-store.sh get "<key>"` → Graylog PAT (fallback key `${USER}_Graylog_Access_Token`). Auth is HTTP Basic with the token as username and the literal `token` as password; the token is fed to `curl` only via a `-K` config file (process substitution), never on argv.
|
|
268
|
-
- Fetch logs (VPN required; respect `vpnServices.graylog`): call `~/.claude/lib/fetch-graylog.sh --trx <id>` (or `--conv <id>` for conversation ids) once per extracted id. The fetcher emits a normalized JSON object of matching log messages.
|
|
269
|
-
- Store as `state.graylogContext = { idType, id, messages[], fetchedAt }` (or an array keyed per id when several were extracted).
|
|
270
|
-
- Phase 1 Analysis prepends `state.graylogContext` to the agent prompt under the **Referenced External Sources** section as **diagnostic context, advisory only** (never contradicts code; no task is generated from it, no Phase 4 gate).
|
|
271
|
-
- Soft-fail (non-blocking, mirrors Crashlytics): unreachable host / VPN down / network timeout → the fetcher emits a normalized empty object and exits `0`; mark `state.graylogContext = { skipped: "vpn-unreachable" }` and continue. Only a genuine auth rejection (4xx on a reachable host, fetcher exit `3`) is logged as `skipped` with the auth reason. A log fetch never blocks the run.
|
|
272
|
-
|
|
273
|
-
##### Step 1b.4 - Other link types (catalogued only at Phase 0; fetched at Phase 1)
|
|
274
|
-
|
|
275
|
-
For `swagger`, `confluence`, `figma`, `generic-doc` entries in `state.contextLinks[]`, no Phase 0 fetch runs. The entries pass through to Phase 1, which dispatches the matching fetcher (Block B work: `fetch-swagger.sh`, `fetch-confluence.sh`, etc.). Until those fetchers land, the entries are surfaced in Phase 1 context as "referenced but not fetched - agent should treat the URL as an authoritative external source and read it directly when relevant."
|
|
276
|
-
|
|
277
|
-
**Jira project key addendum**: Support multi-tenant Jira - `prefs.projects[{project}].jiraProjectKeys[]` is already a list. When Step 1 extracts a Jira key from input, `upsert` it at the head (dedup, cap 10). Sibling keys for the same project (e.g. one key per team or tracker board) coexist; `prefs.global.defaultJiraKey` is the fallback for ambiguous free-text input only.
|
|
278
|
-
|
|
279
|
-
**Log line shape** (progress contract):
|
|
280
|
-
|
|
281
|
-
```
|
|
282
|
-
→ context links: total=<N>, by-type={swagger:<n>, confluence:<n>, crashlytics:<n>, fortify:<n>, graylog:<n>, figma:<n>, generic-doc:<n>}
|
|
283
|
-
→ URL deep fetch: crashlytics=<hit|miss|skipped>, fortify=<hit|miss|skipped>, graylog=<hit|miss|skipped>
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Two lines emit at the end of Step 1b. The first reports the catalogue from 1b.0 (always runs). The second reports the deep-fetch outcomes from 1b.1 / 1b.2 / 1b.3 - `miss` = type not present in catalogue (normal), `skipped` = present but auth/VPN/mismatch skipped the fetch (for graylog this includes the non-blocking VPN-unreachable degrade), `hit` = context stored in state.
|
|
205
|
+
When it runs, load `$HOME/.claude/multi-agent-refs/features/url-enrichment.md` and follow it. It covers: 1b.0 extract + catalogue every link into `state.contextLinks[]` (always runs when this step runs), then the deep fetches that are each conditional on their link type being present - 1b.1 Crashlytics -> `state.crashContext`, 1b.2 Fortify SSC -> `state.fortifyFinding`, 1b.3 Graylog, 1b.4 catalogue-only types (fetched at Phase 1). It also owns the two closing log lines and the Phase 1 / Phase 2 prepend contracts.
|
|
287
206
|
|
|
288
207
|
#### Step 2 - Project Selection
|
|
289
208
|
|
|
@@ -487,6 +406,7 @@ done
|
|
|
487
406
|
|
|
488
407
|
State file in multi-repo mode:
|
|
489
408
|
- Single shared `agent-state.json` lives at `$HOME/.claude/logs/multi-agent/{first-project}/{task-id}/agent-state.json` (anchored on the first repo for back-compat with `multi-agent log`/`status` commands)
|
|
409
|
+
- **One shared file + per-repo writers is exactly the race `write-state.mjs` exists for.** Every update to it (here and in every later phase) goes through `node $HOME/.claude/scripts/write-state.mjs` per the required mechanism in `operations.md` "Writing `agent-state.json`". A read-modify-write from two repos in the same loop drops one repo's `projects[]` entry.
|
|
490
410
|
- `state.projects[]` holds per-repo `{name, root, worktreePath, branch, baseBranch, identity, platform, baseFetchStatus, commit, pr, pushAttempts, buildStatus}` - see `agent-state.schema.json`
|
|
491
411
|
- Scalar fields (`project`, `projectRoot`, `worktreePath`, `branch`, `baseBranch`, `identity`) mirror `projects[0]` so legacy phases that read scalars keep working
|
|
492
412
|
- Atomicity: if any repo's worktree creation fails (collision aborted, fetch aborted, disk full), roll back already-created worktrees: `git -C $proj worktree remove --force $WT_PATH; git -C $proj branch -D $BRANCH`. Never leave a partial multi-repo state.
|
|
@@ -13,7 +13,7 @@ Phase 2 Planning consumes the analysis document. MCP forbidden.
|
|
|
13
13
|
- Abort with `ERR: analysis doc missing. Run /multi-agent:analysis "<feature>" first.`
|
|
14
14
|
|
|
15
15
|
2. **Parse YAML front-matter**: `feature`, `platform`, `language`, `mode`, `evidence_digest`, `template_version`. Persist into `state.analysis.frontMatter`.
|
|
16
|
-
-
|
|
16
|
+
- **`template_version` < `v3` aborts**: `ERR: analysis doc is template v<n>; v3 required. Re-run /multi-agent:analysis to regenerate.` No degraded mode.
|
|
17
17
|
|
|
18
18
|
3. **Section coverage check**: verify these sections are non-empty (template v3 required sections per Locked 2):
|
|
19
19
|
- Section 1 Summary, Section 2 Goals + Non-Goals, Section 4 User Stories, Section 9 API Contracts, Section 13 Architecture Plan, Section 14 Files to Add, Section 20 Risks, Section 21 References
|
|
@@ -16,8 +16,7 @@ Pre-flight steps (run in order, abort on failure):
|
|
|
16
16
|
2. **Parse YAML front-matter** (the doc has a `template_version: v3` header):
|
|
17
17
|
- `feature`, `platform`, `language`, `mode (full | lite)`, `evidence_digest`
|
|
18
18
|
- Persist into `state.analysis.frontMatter`
|
|
19
|
-
- **Abort condition
|
|
20
|
-
- **Soft-skip (v9.1.0+, Locked 30)**: when `prefs.global.legacyV2AnalysisAllowed == true` OR env `MULTI_AGENT_LEGACY_V2_ANALYSIS=allow` is set AND `template_version == v2`, emit `WARN: legacy v2 analysis doc detected; v3 expected. Continuing with reduced binding strength.` and persist `state.dev.legacyAnalysisMode = true`. Step 4 (Code Connect lookup) and Step 6 (conventions handoff) still run, but Step 5 (standards binding) and Pass B footnote enforcement degrade to warning level (Phase 4 reviewer flags missing citations rather than blocking). Escape hatch removed in v9.2.0.
|
|
19
|
+
- **Abort condition**: `template_version` < `v3` -> exit with `ERR: analysis doc is template v<n>; v3 required. Re-run /multi-agent:analysis to regenerate.` No degraded mode.
|
|
21
20
|
|
|
22
21
|
3. **Evidence digest match (cache validation)**: if `state.run.lastAnalysisDigest` is present AND matches `frontMatter.evidence_digest`, mark as fresh; otherwise WARN that analysis is stale (continue but flag).
|
|
23
22
|
|
|
@@ -112,11 +112,16 @@ Cost ledger: `phase-4.repo_map_emitted bytes=N budget=B cache_hit=true|false` -
|
|
|
112
112
|
|
|
113
113
|
Before dispatching reviewers, run the deterministic diff risk scorer and inject the top-N files into each reviewer's prompt as a priority hint. **Advisory only - never gates the pipeline.** Heuristic, zero LLM, runs in well under a second.
|
|
114
114
|
|
|
115
|
+
Score the diff **once, in full** (no `--top`): Steps 1.76/1.77 need every scored file (a shrinking test file ranked 20th; whether ANY file is high-stakes). The top-N hint is derived from that report, not a second git walk.
|
|
116
|
+
|
|
115
117
|
```bash
|
|
116
|
-
|
|
118
|
+
RISK_FULL=$(node pipeline/scripts/diff-risk-score.mjs \
|
|
117
119
|
--base "$BASE_BRANCH" --head HEAD \
|
|
118
|
-
--task-id "$TASK_ID"
|
|
119
|
-
echo "$
|
|
120
|
+
--task-id "$TASK_ID" 2>/dev/null)
|
|
121
|
+
echo "$RISK_FULL" | node pipeline/scripts/validate-diff-risk.mjs - >/dev/null 2>&1 || RISK_FULL=""
|
|
122
|
+
|
|
123
|
+
# Priority hint for the reviewer prompts: top 5 of the full report.
|
|
124
|
+
RISK_JSON=$([ -n "$RISK_FULL" ] && jq -c '.files |= (sort_by(-.score) | .[:5])' <<< "$RISK_FULL" || echo "")
|
|
120
125
|
```
|
|
121
126
|
|
|
122
127
|
**Signals & weights** (see `pipeline/schemas/diff-risk.schema.json`):
|
|
@@ -151,6 +156,35 @@ pipeline/scripts/log-metric.sh "$TASK_ID" 4 review.diff_risk \
|
|
|
151
156
|
|
|
152
157
|
**Opt-out**: `prefs.global.diffRiskAdvisory = false` skips this step entirely (no script invocation, no priority block injection). Default `true` because the cost is bounded and the signal-to-noise has been measured against the golden-task fixture set.
|
|
153
158
|
|
|
159
|
+
#### Step 1.76 - Test-integrity gate (produces BLOCKING findings)
|
|
160
|
+
|
|
161
|
+
Step 1.75 uses `test_lines_removed` as an advisory hint only - too weak for what it detects: a suite made green by deleting tests instead of fixing code. This turns the signal into blocking findings triage must adjudicate. Pure function of the full report, no git, no LLM.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
TEST_INTEGRITY_JSON=$(printf '%s' "$RISK_FULL" | node pipeline/scripts/test-integrity-gate.mjs 2>/dev/null || echo "")
|
|
165
|
+
TI_COUNT=$(jq -r '.count // 0' <<< "${TEST_INTEGRITY_JSON:-{\}}" 2>/dev/null || echo 0)
|
|
166
|
+
[ "$TI_COUNT" -gt 0 ] && pipeline/scripts/log-metric.sh "$TASK_ID" 4 review.test_integrity findings="$TI_COUNT"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`findings[]` are reviewer-shaped (`test_integrity`, `blocking`), so they merge into the reviewer findings at Step 3.0 and need no triage-prompt or `validate-triage.mjs` change. Triage keeps each blocking unless the removal is justified per the immutable-test rule (spec changed AND commit body names the test) → `deferred[]`.
|
|
170
|
+
|
|
171
|
+
The gate never blocks the phase; it *emits* blocking findings. Empty or unreadable input yields zero findings and the phase continues. Feed it the FULL report - `--top` hides a shrinking test file below the cut. **No opt-out**: a run that can switch off its own anti-reward-hacking control cannot be trusted to report a pass.
|
|
172
|
+
|
|
173
|
+
#### Step 1.77 - Reviewer scope (cost gate)
|
|
174
|
+
|
|
175
|
+
On a trivial diff every reviewer agrees and the extra models plus triage are paid for nothing. Reviewer count comes from the same report, no LLM.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
SCOPE_JSON=$(printf '%s' "$RISK_FULL" | node pipeline/scripts/review-scope.mjs 2>/dev/null \
|
|
179
|
+
|| echo '{"scope":"full","reason":"no risk report - failing safe"}')
|
|
180
|
+
REVIEW_SCOPE=$(jq -r '.scope // "full"' <<< "$SCOPE_JSON")
|
|
181
|
+
pipeline/scripts/log-metric.sh "$TASK_ID" 4 review.scope scope="$REVIEW_SCOPE"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`single` (Reviewer 1 only) requires **all** of: churn <= 20 lines, `totals.max_score` < 3.0, and no `security_path` / `migration` / `public_api` / `no_test_change` / `test_lines_removed` on any file. Anything else → `full`.
|
|
185
|
+
|
|
186
|
+
Fails safe in one direction only: empty report, parse failure or validator rejection all resolve to `full`, because skipping a reviewer trades coverage for cost. `consensus.reviewerCount` records what actually ran, so a single-reviewer run never reads as cross-model agreement. Opt-out: `prefs.global.reviewScopeGate = false` forces `full`.
|
|
187
|
+
|
|
154
188
|
#### Step 1.8 - Figma visual-fidelity context (when task carries a Figma reference)
|
|
155
189
|
|
|
156
190
|
When `state.evidence.figma[]` is non-empty, the reviewer subagents MUST receive the captured screenshot URLs / paths and the canonical-component name (from `state.evidence.figma[i].screenshotUrl` and `state.evidence.figma[i].codeConnectSnippets[0].componentName` when present) so they can compare visual fidelity. Pass them inline in each reviewer prompt under a `## Figma evidence` block, one row per frame.
|
|
@@ -182,6 +216,8 @@ Phase 4 sends the same diff to every reviewer and then to triage, so the diff is
|
|
|
182
216
|
|
|
183
217
|
Launch Agent instances **in parallel** using the shared `code-reviewer` subagent definition (`~/.claude/agents/code-reviewer.md`). The reviewer set is determined by the host CLI - GPT-5.4 is only available on Copilot CLI, so Claude Code skips that reviewer and runs a 2-model parallel review; Copilot CLI runs all three.
|
|
184
218
|
|
|
219
|
+
**Scope from Step 1.77.** `$REVIEW_SCOPE == "single"` → dispatch **Reviewer 1 only**, and skip Step 2.5 + 3.6 (both no-ops with one reviewer). `"full"` (default + fail-safe) → the whole set below. Either way record the count in `consensus.reviewerCount`.
|
|
220
|
+
|
|
185
221
|
| Reviewer | subagent_type | Model | Focus | Skills Referenced | Where it runs |
|
|
186
222
|
| ---------- | ----------------- | ------------------- | --------------------------------- | --------------------------------------------- | -------------------- |
|
|
187
223
|
| Reviewer 1 | `code-reviewer` | `claude-fable-5` (Claude Code) / `claude-opus-4-8` (Copilot CLI) | Deep security + architecture | `api-security-best-practices`, `architecture` | Both CLIs |
|
|
@@ -259,9 +295,18 @@ Exit 0 = valid. Exit 2 = contradiction (approved=true with blocking findings) -
|
|
|
259
295
|
|
|
260
296
|
Opt-in empirical layer: when `prefs.global.verifyByTest.enabled` is `true`, accepted blocking findings additionally go through Step 3.7 (verify-by-test), which tries to reproduce each one with a minimal failing test before the Phase 3 rework loop fires. Full wiring: `$HOME/.claude/multi-agent-refs/features/verify-by-test.md`.
|
|
261
297
|
|
|
298
|
+
##### 3.0 Merge the deterministic findings in
|
|
299
|
+
|
|
300
|
+
Append the Step 1.76 test-integrity findings to the reviewer findings before counting, so they are adjudicated like any reviewer finding rather than resolved by triage never seeing them:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
MERGED=$(jq -s '.[0] + (.[1].findings // [])' \
|
|
304
|
+
<(printf '%s' "$REVIEWER_FINDINGS_JSON") <(printf '%s' "${TEST_INTEGRITY_JSON:-{\}}"))
|
|
305
|
+
```
|
|
306
|
+
|
|
262
307
|
##### 3.1 Short-circuit: no findings
|
|
263
308
|
|
|
264
|
-
If merged findings `length === 0`, **skip triage**: write empty result `{"accepted": [], "deferred": [], "rejected": [], "approved": true}`, log, proceed to Phase 5.
|
|
309
|
+
If **merged** findings `length === 0`, **skip triage**: write empty result `{"accepted": [], "deferred": [], "rejected": [], "approved": true}`, log, proceed to Phase 5. Note this is the merged count from 3.0: a run with zero reviewer findings but a non-empty test-integrity set must NOT short-circuit.
|
|
265
310
|
|
|
266
311
|
##### 3.2 Launch triage agent
|
|
267
312
|
|
|
@@ -429,64 +474,7 @@ Log: "Phase 4: Review - raw={N1+N2} accepted={Na} deferred={Nd} rejected={Nr}
|
|
|
429
474
|
|
|
430
475
|
#### Multi-Repo Mode
|
|
431
476
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
**Combined diff assembly** - reviewers get **one combined diff** covering all repos, with explicit per-repo headers so they can flag cross-repo impact:
|
|
435
|
-
|
|
436
|
-
```bash
|
|
437
|
-
COMBINED_DIFF=$(mktemp)
|
|
438
|
-
for proj in $(jq -r '.projects[] | "\(.name)\t\(.worktreePath)\t\(.baseBranch)"' "$STATE_FILE"); do
|
|
439
|
-
IFS=$'\t' read -r name wt base <<< "$proj"
|
|
440
|
-
ORIGIN=$(git -C "$wt" config --get remote.origin.url)
|
|
441
|
-
printf '\n=== repo: %s (origin: %s) ===\n' "$name" "$ORIGIN" >> "$COMBINED_DIFF"
|
|
442
|
-
printf 'Base: %s ... HEAD\n\n' "$base" >> "$COMBINED_DIFF"
|
|
443
|
-
git -C "$wt" diff "origin/$base...HEAD" >> "$COMBINED_DIFF"
|
|
444
|
-
done
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
Same reviewer set (Fable-or-Opus / GPT-5.4 / Sonnet) receive `COMBINED_DIFF` with a multi-repo prefix in the system prompt:
|
|
448
|
-
|
|
449
|
-
```
|
|
450
|
-
This is a multi-repo task spanning {N} repos: {repo names}.
|
|
451
|
-
Each repo's diff is prefixed with `=== repo: <name> ===`.
|
|
452
|
-
A finding may span repos (e.g. "common adds API X, but uicomponents calls X with wrong type").
|
|
453
|
-
For cross-repo findings, set `crossRepo: true` and list all impacted repos in `affectedRepos: []`.
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**Schema extension for findings** (additive, single-repo `crossRepo` defaults to false):
|
|
457
|
-
|
|
458
|
-
```json
|
|
459
|
-
{
|
|
460
|
-
"severity": "blocking|important|suggestion",
|
|
461
|
-
"file": "uicomponents/Sources/Foo.swift",
|
|
462
|
-
"line": 42,
|
|
463
|
-
"issue": "...",
|
|
464
|
-
"fix": "...",
|
|
465
|
-
"crossRepo": false,
|
|
466
|
-
"affectedRepos": []
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
`file` paths are repo-name-prefixed (`<repo-name>/<path-relative-to-repo>`) so triage and rework can resolve them back to the right worktree. The triage prompt receives `state.projects[]` so it can validate `affectedRepos` entries.
|
|
471
|
-
|
|
472
|
-
**Triage cross-check**: Cross-repo findings are NEVER auto-rejected - the cross-cutting nature is exactly what reviewers are best at catching. Triage may still defer (out-of-scope) but rejection requires explicit reasoning.
|
|
473
|
-
|
|
474
|
-
**Token-budget guard** (per `token-budget.json` Phase 4 allowance): if `COMBINED_DIFF` exceeds 80% of the budget, truncate the largest repo's diff with a footer header:
|
|
475
|
-
```
|
|
476
|
-
[truncated - full diff in file://$WORKTREE/.review-diff.txt]
|
|
477
|
-
```
|
|
478
|
-
And log `review.diff_truncated repo=<name> bytes_dropped=<N>`. Triage receives the same truncated view + the truncation marker so it can flag suggestions to "review the full diff manually."
|
|
479
|
-
|
|
480
|
-
**Deterministic gates per repo**: Step 1 gates (build/lint/test/secrets) run **per repo** - failure in any one repo blocks AI review for the whole task. Build queue lock applies as in Phase 3 (multi-repo doesn't change Xcode serialization).
|
|
481
|
-
|
|
482
|
-
**Telemetry**: Per-repo build/test gate timings + a single combined review/triage call set:
|
|
483
|
-
```bash
|
|
484
|
-
pipeline/scripts/log-metric.sh "$TASK_ID" 4 gate.build repo=common status=pass duration_ms=$D
|
|
485
|
-
pipeline/scripts/log-metric.sh "$TASK_ID" 4 gate.build repo=uicomponents status=pass duration_ms=$D
|
|
486
|
-
pipeline/scripts/log-metric.sh "$TASK_ID" 4 review.combined_diff repos=2 bytes=$BYTES truncated=false
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
---
|
|
477
|
+
**Only when `state.projects[].length > 1`.** Load `$HOME/.claude/multi-agent-refs/features/review-multi-repo.md` and follow it: per-repo diff scoping, how one merged finding set is attributed back to the repo that owns each file, and the per-repo `buildStatus` contract. A single-repo run skips this section entirely.
|
|
490
478
|
|
|
491
479
|
## Token telemetry - invoke after every LLM call
|
|
492
480
|
|
|
@@ -960,6 +960,12 @@
|
|
|
960
960
|
"default": true,
|
|
961
961
|
"description": "v8.3+ - Phase 4 Step 1.75 advisory diff risk scoring. When enabled, `pipeline/scripts/diff-risk-score.mjs` runs before reviewer dispatch and the top-N risk-ranked files are injected into each reviewer's prompt as a priority hint (security paths, public API surfaces, untested source changes, schema migrations). Heuristic, deterministic, no LLM cost. Default ON - the run is sub-second and never gates the pipeline. Flip to false to skip the script and the prompt injection entirely."
|
|
962
962
|
},
|
|
963
|
+
"reviewScopeGate": {
|
|
964
|
+
"type": "boolean",
|
|
965
|
+
"default": true,
|
|
966
|
+
"description": "v12.8+ - Phase 4 Step 1.77 reviewer-scope gate. Decides the reviewer count from the deterministic diff-risk report via `pipeline/scripts/review-scope.mjs`: a diff under 20 lines of churn with max_score < 3.0 and no security_path / migration / public_api / no_test_change / test_lines_removed signal runs ONE reviewer instead of the full CLI-aware set (2 on Claude Code, 3 on Copilot CLI). No LLM. Fails safe in one direction only - any error, empty report or validator rejection resolves to the full set, because skipping a reviewer trades coverage for cost. Set false to force the full set on every diff.",
|
|
967
|
+
"$comment": "Shipped inert for a release: the script existed, was unit- and smoke-tested, and no phase doc referenced it, so every diff paid for the full reviewer set. Wired in Step 1.77; smoke-gate-wiring.sh keeps it reachable."
|
|
968
|
+
},
|
|
963
969
|
"priorArtEnrichment": {
|
|
964
970
|
"type": "object",
|
|
965
971
|
"additionalProperties": false,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# _smoke-root.sh - resolve the pipeline tree for a smoke that must run in BOTH
|
|
4
|
+
# the repo checkout and an end-user install.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists: the repo and an install have different shapes.
|
|
7
|
+
#
|
|
8
|
+
# repo checkout install (~/.claude or ~/.copilot)
|
|
9
|
+
# ------------------ --------------------------------
|
|
10
|
+
# pipeline/scripts/ scripts/
|
|
11
|
+
# pipeline/schemas/ schemas/
|
|
12
|
+
# pipeline/multi-agent-refs/ multi-agent-refs/
|
|
13
|
+
# pipeline/commands/ commands/
|
|
14
|
+
# package.json (absent)
|
|
15
|
+
#
|
|
16
|
+
# A smoke that hardcodes `REPO_ROOT="$(dirname "$0")/../.."` and then reads
|
|
17
|
+
# `$REPO_ROOT/pipeline/...` resolves to `$HOME/pipeline/...` once installed, so
|
|
18
|
+
# every path check fails. Whoever ran it sees a red report and concludes the
|
|
19
|
+
# install is broken. Source this file instead and read the MA_* variables.
|
|
20
|
+
#
|
|
21
|
+
# Usage (from any script in pipeline/scripts/):
|
|
22
|
+
# . "$(dirname "${BASH_SOURCE[0]}")/_smoke-root.sh"
|
|
23
|
+
# grep -q needle "$MA_REFS/phases/phase-4-review.md"
|
|
24
|
+
#
|
|
25
|
+
# Exports:
|
|
26
|
+
# MA_LAYOUT "repo" | "install"
|
|
27
|
+
# MA_ROOT repo root, or the install root (~/.claude)
|
|
28
|
+
# MA_SCRIPTS directory holding this script
|
|
29
|
+
# MA_SCHEMAS JSON schemas
|
|
30
|
+
# MA_REFS multi-agent-refs
|
|
31
|
+
# MA_COMMANDS slash-command tree
|
|
32
|
+
# MA_SKILLS skills tree
|
|
33
|
+
#
|
|
34
|
+
# Every exported directory is verified to exist; a missing one is left empty so
|
|
35
|
+
# the caller can skip that check rather than report a false failure.
|
|
36
|
+
|
|
37
|
+
MA_SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
|
|
39
|
+
# The repo is the only layout with pipeline/ beside package.json. Checking both
|
|
40
|
+
# rules out an install that happens to sit next to an unrelated pipeline/ dir.
|
|
41
|
+
_ma_candidate="$(cd "$MA_SCRIPTS/../.." && pwd)"
|
|
42
|
+
if [ -f "$_ma_candidate/package.json" ] && [ -d "$_ma_candidate/pipeline" ]; then
|
|
43
|
+
MA_LAYOUT="repo"
|
|
44
|
+
MA_ROOT="$_ma_candidate"
|
|
45
|
+
_ma_base="$_ma_candidate/pipeline"
|
|
46
|
+
else
|
|
47
|
+
MA_LAYOUT="install"
|
|
48
|
+
MA_ROOT="$(cd "$MA_SCRIPTS/.." && pwd)"
|
|
49
|
+
_ma_base="$MA_ROOT"
|
|
50
|
+
fi
|
|
51
|
+
unset _ma_candidate
|
|
52
|
+
|
|
53
|
+
_ma_dir() { [ -d "$1" ] && echo "$1" || echo ""; }
|
|
54
|
+
|
|
55
|
+
MA_SCHEMAS="$(_ma_dir "$_ma_base/schemas")"
|
|
56
|
+
MA_REFS="$(_ma_dir "$_ma_base/multi-agent-refs")"
|
|
57
|
+
MA_COMMANDS="$(_ma_dir "$_ma_base/commands")"
|
|
58
|
+
MA_SKILLS="$(_ma_dir "$_ma_base/skills")"
|
|
59
|
+
unset _ma_base
|
|
60
|
+
|
|
61
|
+
export MA_LAYOUT MA_ROOT MA_SCRIPTS MA_SCHEMAS MA_REFS MA_COMMANDS MA_SKILLS
|