@mmerterden/multi-agent-pipeline 17.6.0 → 19.0.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 +310 -0
- package/README.md +76 -18
- package/README.tr.md +55 -16
- package/docs/adr/0002-instruction-driven-flag.md +1 -0
- package/docs/adr/0005-lazy-phase-docs.md +11 -1
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +1 -0
- package/docs/adr/0010-own-code-graph.md +1 -0
- package/docs/adr/0011-dormant-ci.md +25 -1
- package/docs/adr/0014-six-phase-consolidation.md +134 -0
- package/docs/adr/README.md +2 -1
- package/docs/architecture.md +37 -38
- package/docs/best-practices.md +1 -1
- package/docs/ecosystem.md +37 -26
- package/docs/engineering.md +1 -1
- package/docs/facts.json +45 -0
- package/docs/features.md +54 -53
- package/docs/performance.md +5 -5
- package/docs/recovery-guide.md +9 -9
- package/docs/server-readiness.md +188 -0
- package/docs/token-budget-history.md +3 -1
- package/index.js +18 -3
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +42 -17
- package/install/_dev-only-files.mjs +8 -0
- package/install/_unattended-profile.mjs +113 -0
- package/install/index.mjs +48 -0
- package/install/templates/claude-hooks.json +1 -1
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +28 -28
- package/manifest.json +1065 -0
- package/package.json +6 -3
- package/pipeline/agents/dev-critic.md +3 -3
- package/pipeline/commands/figma-to-swiftui.md +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/analysis/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +7 -7
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -15
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +6 -6
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/graph/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +62 -62
- package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/local/SKILL.md +11 -11
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +13 -13
- package/pipeline/commands/multi-agent/log/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/model/SKILL.md +69 -0
- package/pipeline/commands/multi-agent/refactor/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/resume/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/resume-local/SKILL.md +19 -17
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/route-off/SKILL.md +36 -0
- package/pipeline/commands/multi-agent/route-on/SKILL.md +74 -0
- package/pipeline/commands/multi-agent/route-status/SKILL.md +56 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/status/SKILL.md +54 -23
- package/pipeline/commands/multi-agent/steer/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -13
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
- package/pipeline/lib/_jira-auth.sh +8 -0
- package/pipeline/lib/analysis-jira-write.sh +32 -0
- package/pipeline/lib/ask-choice.sh +13 -2
- package/pipeline/lib/autopilot-state.sh +8 -0
- package/pipeline/lib/credential-inventory.sh +1 -1
- package/pipeline/lib/fatal.mjs +129 -0
- package/pipeline/lib/fetch-fortify.sh +1 -1
- package/pipeline/lib/figma-mcp-refresh.sh +18 -0
- package/pipeline/lib/figma-screenshot.sh +18 -0
- package/pipeline/lib/invoked-directly.mjs +43 -0
- package/pipeline/lib/jira-publish.sh +42 -0
- package/pipeline/lib/md2confluence-v3.py +47 -0
- package/pipeline/lib/model-rung.sh +142 -0
- package/pipeline/lib/outbound-gate.mjs +175 -0
- package/pipeline/lib/phase-schema.mjs +88 -0
- package/pipeline/lib/plan-todos.sh +32 -11
- package/pipeline/lib/post-pr-review.sh +77 -8
- package/pipeline/lib/repo-hygiene.sh +8 -3
- package/pipeline/lib/require-jq.sh +40 -0
- package/pipeline/lib/route-state.sh +161 -0
- package/pipeline/lib/run-paths.sh +335 -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/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/analysis/evidence.md +0 -9
- package/pipeline/multi-agent-refs/analysis/intake.md +1 -1
- package/pipeline/multi-agent-refs/analysis/locked.md +21 -22
- package/pipeline/multi-agent-refs/analysis/render.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +12 -6
- package/pipeline/multi-agent-refs/android-guide.md +1 -1
- package/pipeline/multi-agent-refs/audit-guide.md +13 -13
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +3 -3
- package/pipeline/multi-agent-refs/channels/pr.md +4 -4
- package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +3 -3
- package/pipeline/multi-agent-refs/cross-cli-contract.md +31 -6
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +74 -4
- package/pipeline/multi-agent-refs/features/code-graph.md +5 -5
- package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
- package/pipeline/multi-agent-refs/features/design-conformance.md +1 -1
- package/pipeline/multi-agent-refs/features/dev-critic.md +3 -3
- package/pipeline/multi-agent-refs/features/doctor.md +47 -2
- package/pipeline/multi-agent-refs/features/external-context-injection.md +3 -3
- package/pipeline/multi-agent-refs/features/maturity-followup.md +3 -3
- package/pipeline/multi-agent-refs/features/model-fallback.md +5 -5
- package/pipeline/multi-agent-refs/features/plan-todos.md +1 -1
- package/pipeline/multi-agent-refs/features/repo-map.md +1 -1
- package/pipeline/multi-agent-refs/features/review-delta.md +3 -3
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +4 -4
- package/pipeline/multi-agent-refs/features/skill-conformance.md +2 -2
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
- package/pipeline/multi-agent-refs/features/verify-by-test.md +4 -4
- package/pipeline/multi-agent-refs/features/verify.md +83 -0
- package/pipeline/multi-agent-refs/features/visual-evidence.md +19 -19
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +6 -6
- package/pipeline/multi-agent-refs/issue-jira-triad.md +10 -10
- package/pipeline/multi-agent-refs/knowledge.md +11 -11
- package/pipeline/multi-agent-refs/multi-repo-integration-build.md +13 -13
- package/pipeline/multi-agent-refs/payload-contracts.md +8 -8
- package/pipeline/multi-agent-refs/phases/log-format.md +10 -10
- package/pipeline/multi-agent-refs/phases/modes.md +30 -30
- package/pipeline/multi-agent-refs/phases/operations.md +21 -10
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +25 -25
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +599 -0
- package/pipeline/multi-agent-refs/phases/{phase-3-dev.md → phase-2-dev.md} +129 -49
- package/pipeline/multi-agent-refs/phases/{phase-4-review.md → phase-3-review.md} +225 -107
- package/pipeline/multi-agent-refs/phases/{phase-6-commit.md → phase-4-commit.md} +23 -23
- package/pipeline/multi-agent-refs/phases/{phase-7-report.md → phase-5-report.md} +29 -29
- package/pipeline/multi-agent-refs/phases.md +44 -48
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/progress-contract.md +6 -6
- package/pipeline/multi-agent-refs/readiness-review.md +1 -1
- package/pipeline/multi-agent-refs/rules.md +7 -7
- package/pipeline/multi-agent-refs/swiftui-guide.md +2 -2
- package/pipeline/multi-agent-refs/tracker-contract.md +31 -32
- package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
- package/pipeline/multi-agent-refs/wiki-capture.md +14 -14
- package/pipeline/preferences-template.json +9 -1
- package/pipeline/rules/outside-the-pipeline.md +1 -1
- package/pipeline/schemas/agent-state.schema.json +50 -50
- package/pipeline/schemas/analysis-output.schema.json +2 -2
- package/pipeline/schemas/autopilot-config.schema.json +1 -1
- package/pipeline/schemas/code-graph.schema.json +1 -1
- package/pipeline/schemas/criteria-manifest.schema.json +1 -1
- package/pipeline/schemas/dev-critic-output.schema.json +1 -1
- package/pipeline/schemas/diff-risk.schema.json +1 -1
- package/pipeline/schemas/migrations/prefs-2.4.0-to-2.5.0.mjs +2 -2
- package/pipeline/schemas/migrations/prefs-2.6.0-to-2.7.0.mjs +31 -0
- package/pipeline/schemas/migrations/state-2.1.0-to-2.2.0.mjs +129 -0
- package/pipeline/schemas/phases.json +105 -0
- package/pipeline/schemas/plan-todos.schema.json +5 -5
- package/pipeline/schemas/planning-output.schema.json +1 -1
- package/pipeline/schemas/prefs.schema.json +100 -56
- package/pipeline/schemas/reviewer-output.schema.json +3 -3
- package/pipeline/schemas/route-config.schema.json +74 -0
- package/pipeline/schemas/scope-check.schema.json +1 -1
- package/pipeline/schemas/test-gap.schema.json +1 -1
- package/pipeline/schemas/token-budget.json +12 -18
- package/pipeline/schemas/triage-output.schema.json +6 -6
- package/pipeline/scripts/README.md +3 -3
- package/pipeline/scripts/_code-graph.mjs +2 -2
- package/pipeline/scripts/_run-paths.mjs +372 -0
- package/pipeline/scripts/_smoke-root.sh +1 -1
- package/pipeline/scripts/aggregate-metrics.mjs +65 -65
- package/pipeline/scripts/autopilot-arming.mjs +2 -1
- package/pipeline/scripts/autopilot-intake.mjs +2 -1
- package/pipeline/scripts/autopilot-runner.mjs +206 -2
- package/pipeline/scripts/build-references.mjs +2 -1
- package/pipeline/scripts/build-stack-plugins.mjs +10 -2
- package/pipeline/scripts/capture-evidence.sh +7 -2
- package/pipeline/scripts/capture-flush.sh +8 -8
- package/pipeline/scripts/capture-resume.sh +3 -3
- package/pipeline/scripts/classify-plan-safety.mjs +3 -2
- package/pipeline/scripts/cost-analyze.mjs +600 -0
- package/pipeline/scripts/cost-budget-check.mjs +4 -12
- package/pipeline/scripts/council-view.mjs +2 -1
- package/pipeline/scripts/crush-json.mjs +2 -1
- package/pipeline/scripts/diff-explain.mjs +7 -10
- package/pipeline/scripts/diff-risk-score.mjs +2 -1
- package/pipeline/scripts/doctor.mjs +140 -6
- package/pipeline/scripts/evidence-gate.mjs +9 -3
- package/pipeline/scripts/feedback-send.mjs +12 -2
- package/pipeline/scripts/gc-abandoned.sh +32 -16
- package/pipeline/scripts/gc-tmp.sh +1 -1
- package/pipeline/scripts/gc-worktrees.sh +12 -5
- package/pipeline/scripts/gen-facts.mjs +175 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +32 -37
- package/pipeline/scripts/gen-ref-toc.mjs +1 -1
- package/pipeline/scripts/github-ssh-setup.sh +64 -7
- package/pipeline/scripts/graph-mermaid.mjs +4 -2
- package/pipeline/scripts/graph-report.mjs +1 -1
- package/pipeline/scripts/jira-attach.sh +1 -1
- package/pipeline/scripts/keychain-save.sh +101 -30
- package/pipeline/scripts/learn-from-transcripts.mjs +3 -2
- package/pipeline/scripts/learning-curve.mjs +36 -31
- package/pipeline/scripts/log-metric.sh +17 -4
- package/pipeline/scripts/make-manifest.mjs +199 -0
- package/pipeline/scripts/memory-save.sh +1 -1
- package/pipeline/scripts/migrate-prefs.mjs +24 -6
- package/pipeline/scripts/migrate-state.mjs +94 -4
- package/pipeline/scripts/phase-banner.sh +26 -22
- package/pipeline/scripts/phase-tracker.sh +48 -10
- package/pipeline/scripts/plan-coverage-gate.mjs +8 -4
- package/pipeline/scripts/pre-commit-check.sh +7 -0
- package/pipeline/scripts/pre-push-check.sh +7 -0
- package/pipeline/scripts/purge.sh +23 -6
- package/pipeline/scripts/render-agent-log-cost.sh +10 -3
- package/pipeline/scripts/render-cost-summary.sh +9 -2
- package/pipeline/scripts/render-work-summary.sh +14 -7
- package/pipeline/scripts/review-file-filter.mjs +5 -3
- package/pipeline/scripts/review-scope.mjs +2 -1
- package/pipeline/scripts/routine-registry.mjs +2 -1
- package/pipeline/scripts/run-aggregator.mjs +26 -20
- package/pipeline/scripts/run-metrics.mjs +4 -2
- package/pipeline/scripts/runs-index.mjs +353 -0
- package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
- package/pipeline/scripts/search-logs.sh +18 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +6 -6
- package/pipeline/scripts/smoke-schema-validation.sh +26 -7
- package/pipeline/scripts/test-gap-scan.mjs +2 -1
- package/pipeline/scripts/test-integrity-gate.mjs +2 -1
- package/pipeline/scripts/token-budget-report.mjs +13 -2
- package/pipeline/scripts/triage-memory.mjs +2 -2
- package/pipeline/scripts/update-issue-progress.sh +56 -7
- package/pipeline/scripts/usage-report.mjs +12 -1
- package/pipeline/scripts/validate-analysis-doc.mjs +75 -18
- package/pipeline/scripts/validate-code-graph.mjs +6 -3
- package/pipeline/scripts/validate-complaint-doc.mjs +2 -1
- package/pipeline/scripts/validate-diff-risk.mjs +6 -3
- package/pipeline/scripts/validate-planning.mjs +1 -1
- package/pipeline/scripts/validate-reviewer.mjs +1 -1
- package/pipeline/scripts/validate-state.mjs +45 -5
- package/pipeline/scripts/validate-test-gap.mjs +6 -3
- package/pipeline/scripts/validate-triage.mjs +6 -4
- package/pipeline/scripts/verify-citations.mjs +4 -2
- package/pipeline/scripts/verify.mjs +327 -0
- package/pipeline/scripts/worktree-finalize.sh +18 -9
- package/pipeline/scripts/write-state.mjs +154 -15
- package/pipeline/skills/.skill-manifest.json +37 -21
- package/pipeline/skills/.skills-index.json +104 -5
- package/pipeline/skills/shared/README.md +15 -6
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +2 -2
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +69 -71
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +14 -14
- package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +5 -5
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +25 -23
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +71 -0
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-resume-local/SKILL.md +7 -7
- package/pipeline/skills/shared/core/multi-agent-route-off/SKILL.md +39 -0
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +76 -0
- package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +59 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +35 -11
- package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/package_app.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/setup_dev_signing.sh +4 -1
- package/pipeline/skills/shared/external/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +2 -1
- package/pipeline/skills/skills-index.md +13 -4
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +0 -263
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +0 -344
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +0 -182
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/scope-check.schema.json",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"title": "Multi-Agent Pipeline - Phase 3 scope self-check",
|
|
6
|
-
"description": "Written by Phase
|
|
6
|
+
"description": "Written by Phase 2 Step 3.7 to $WORKTREE/.pipeline/scope-check.json before the Phase 4 handoff: one stated reason per file in the diff, the changes deliberately not made, and the code-simplifier rationales. scope-check-gate.mjs compares files[] with the real diff; Phase 4 injects the record as <scope-self-check>; Phase 4 builds the PR Changes bullets and the follow-up list from it.",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"required": ["version", "taskId", "files"],
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/test-gap.schema.json",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"title": "Multi-Agent Pipeline - Phase
|
|
5
|
+
"title": "Multi-Agent Pipeline - Phase 3 test gap report",
|
|
6
6
|
"description": "Output of test-gap-scan.mjs. Lists symbols added/changed in the diff that have no paired test file or no matching test method. Advisory in default mode; opt-in blocking via prefs.testGap.blockingThreshold.",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/token-budget.json",
|
|
4
|
-
"description": "Per-phase token ceilings for the lazy-loaded pipeline docs, enforced by smoke-token-budget.sh. Only the ACTIVE phase is loaded at run time and nothing truncates a phase document, so these numbers govern what may be written, not what a run receives. Two rules, and the split is the point. `max_tokens` and `total_max_tokens` are committed constants a human owns: computing them would end the gate, because a ceiling that is always current x k can never fail. The warn tier is NOT stored - the gate derives it per phase from that phase's own git history (median + 3*MAD of its historical per-commit deltas, which is robust to the one 980-token release that makes sigma meaningless on phase-4), because a soft line maintained by hand rots, and this one rotted twice: it was reset at v13.6.0 when five lines had gone permanently amber, and six of eight were amber again by v17.1.0. A ceiling more than 25% above the measurement is stale and fails, which is the 'only ratchets down' rule the SKILL.md grace list always had and this budget never did. Change history: docs/token-budget-history.md.",
|
|
4
|
+
"description": "Per-phase token ceilings for the lazy-loaded pipeline docs, enforced by smoke-token-budget.sh. Only the ACTIVE phase is loaded at run time and nothing truncates a phase document, so these numbers govern what may be written, not what a run receives. Two rules, and the split is the point. `max_tokens` and `total_max_tokens` are committed constants a human owns: computing them would end the gate, because a ceiling that is always current x k can never fail. The warn tier is NOT stored - the gate derives it per phase from that phase's own git history (median + 3*MAD of its historical per-commit deltas, which is robust to the one 980-token release that makes sigma meaningless on phase-4), because a soft line maintained by hand rots, and this one rotted twice: it was reset at v13.6.0 when five lines had gone permanently amber, and six of eight were amber again by v17.1.0. A ceiling more than 25% above the measurement is stale and fails, which is the 'only ratchets down' rule the SKILL.md grace list always had and this budget never did. Ceilings are DERIVED from schemas/phases.json and re-measured after the v19.0.0 six-phase merge, not summed from the eight old ones: the two-sided rule fails a ceiling more than 25% above the measurement, so a sum would have shipped stale by construction. Change history: docs/token-budget-history.md.",
|
|
5
5
|
"phases": {
|
|
6
6
|
"phase-0-init": {
|
|
7
7
|
"max_tokens": 13400
|
|
8
8
|
},
|
|
9
|
-
"phase-1-
|
|
10
|
-
"max_tokens":
|
|
9
|
+
"phase-1-plan": {
|
|
10
|
+
"max_tokens": 10000
|
|
11
11
|
},
|
|
12
|
-
"phase-2-
|
|
13
|
-
"max_tokens":
|
|
14
|
-
},
|
|
15
|
-
"phase-3-dev": {
|
|
16
|
-
"max_tokens": 9450
|
|
12
|
+
"phase-2-dev": {
|
|
13
|
+
"max_tokens": 10650
|
|
17
14
|
},
|
|
18
|
-
"phase-
|
|
19
|
-
"max_tokens":
|
|
15
|
+
"phase-3-review": {
|
|
16
|
+
"max_tokens": 17200
|
|
20
17
|
},
|
|
21
|
-
"phase-
|
|
22
|
-
"max_tokens":
|
|
23
|
-
},
|
|
24
|
-
"phase-6-commit": {
|
|
25
|
-
"max_tokens": 6550
|
|
18
|
+
"phase-4-commit": {
|
|
19
|
+
"max_tokens": 6500
|
|
26
20
|
},
|
|
27
|
-
"phase-
|
|
28
|
-
"max_tokens":
|
|
21
|
+
"phase-5-report": {
|
|
22
|
+
"max_tokens": 5550
|
|
29
23
|
}
|
|
30
24
|
},
|
|
31
|
-
"total_max_tokens":
|
|
25
|
+
"total_max_tokens": 63300
|
|
32
26
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/triage-output.schema.json",
|
|
4
4
|
"version": "3.4.0",
|
|
5
|
-
"title": "Multi-Agent Pipeline - Phase
|
|
6
|
-
"description": "Contract for the Opus triage agent's JSON output in Phase 4 Step 3. Triage consumes merged reviewer findings and splits them into accepted/deferred/rejected. Only `accepted` blocking/important items trigger Phase 3 rework. v3.1.0 adds the optional `consensus` block so triage can surface reviewer-agreement risk (false consensus among same-base-model reviewers) instead of silently merging. v3.2.0 adds the optional per-finding `verification` block written by Phase
|
|
5
|
+
"title": "Multi-Agent Pipeline - Phase 3 triage output",
|
|
6
|
+
"description": "Contract for the Opus triage agent's JSON output in Phase 4 Step 3. Triage consumes merged reviewer findings and splits them into accepted/deferred/rejected. Only `accepted` blocking/important items trigger Phase 3 rework. v3.1.0 adds the optional `consensus` block so triage can surface reviewer-agreement risk (false consensus among same-base-model reviewers) instead of silently merging. v3.2.0 adds the optional per-finding `verification` block written by Phase 3 Step 3.7 (verify-by-test): the empirical repro-test outcome for accepted blocking findings. v3.3.0 carries ruleId + criteriaSource through triage so a finding that cites a stable rule ID keeps that citation into Phase 4 and Phase 5, and the lesson loop can key durable learnings by rule. v3.4.0 adds the optional per-finding fingerprint (finding-fingerprint.mjs) so review-delta.mjs can tell still-present, resolved and new findings apart between rounds.",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"required": ["accepted", "deferred", "rejected", "approved"],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"deferred": {
|
|
19
19
|
"type": "array",
|
|
20
|
-
"description": "Findings that are real but out of current task scope. Surfaced in Phase
|
|
20
|
+
"description": "Findings that are real but out of current task scope. Surfaced in Phase 5 report; not actioned.",
|
|
21
21
|
"items": {
|
|
22
22
|
"type": "object",
|
|
23
23
|
"additionalProperties": false,
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"approved": {
|
|
57
57
|
"type": "boolean",
|
|
58
|
-
"description": "True if no accepted BLOCKING items remain. The pipeline uses this to gate Phase 5 / Phase
|
|
58
|
+
"description": "True if no accepted BLOCKING items remain. The pipeline uses this to gate Phase 5 / Phase 4 entry."
|
|
59
59
|
},
|
|
60
60
|
"consensus": {
|
|
61
61
|
"$ref": "#/$defs/consensus"
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
},
|
|
112
112
|
"disagreements": {
|
|
113
113
|
"type": "array",
|
|
114
|
-
"description": "Findings where reviewers split (existence or severity). Surfaced verbatim in the Phase
|
|
114
|
+
"description": "Findings where reviewers split (existence or severity). Surfaced verbatim in the Phase 5 report and at the Step 4 user checkpoint.",
|
|
115
115
|
"items": {
|
|
116
116
|
"type": "object",
|
|
117
117
|
"additionalProperties": false,
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"verification": {
|
|
143
143
|
"type": "object",
|
|
144
144
|
"additionalProperties": false,
|
|
145
|
-
"description": "v3.2.0 verify-by-test outcome (Phase
|
|
145
|
+
"description": "v3.2.0 verify-by-test outcome (Phase 3 Step 3.7, opt-in via prefs.global.verifyByTest). confirmed = repro test failed as the finding predicts (finding stands, test kept as the Phase 3 RED test); not-reproduced = repro test passed under evidence-gate (finding downgraded to deferred); inconclusive = compile error / timeout / not unit-testable (judgment verdict stands).",
|
|
146
146
|
"required": ["result"],
|
|
147
147
|
"properties": {
|
|
148
148
|
"result": {
|
|
@@ -19,10 +19,10 @@ Validate contracts. Each emits `══ <name> smoke: N passed, M failed ══`
|
|
|
19
19
|
|
|
20
20
|
### Phase contracts
|
|
21
21
|
- `smoke-phase-0-multi-repo.sh` - Phase 0 multi-repo mode fetch + worktree atomicity
|
|
22
|
-
- `smoke-phase-6-multi.sh` - Phase
|
|
22
|
+
- `smoke-phase-6-multi.sh` - Phase 4 multi-repo commit/PR cross-linking
|
|
23
23
|
- `smoke-phase-banner.sh` + `smoke-phase-tracker.sh` - Phase UI output contracts
|
|
24
|
-
- `smoke-phase4-triage.sh` - Phase
|
|
25
|
-
- `smoke-verify-by-test.sh` - Phase
|
|
24
|
+
- `smoke-phase4-triage.sh` - Phase 3 reviewer → triage flow
|
|
25
|
+
- `smoke-verify-by-test.sh` - Phase 3 Step 3.7 verify-by-test contract (v10.8.0)
|
|
26
26
|
- `smoke-handoff-contract.sh` - phase-boundary structured handoff + handoff-first resume (v10.8.0)
|
|
27
27
|
- `smoke-update-check.sh` - Phase 0 Step 0.6 update-check + required-floor contract (v10.9.0, floor v15.14.0)
|
|
28
28
|
- `smoke-context-links.sh` - context-link-extractor classification contract, all types (v15.14.0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file _code-graph.mjs - deterministic, LLM-free code graph for Phase 1 and Phase
|
|
2
|
+
* @file _code-graph.mjs - deterministic, LLM-free code graph for Phase 1 and Phase 5.
|
|
3
3
|
*
|
|
4
|
-
* Phase 1 narrows its Explore fan-out with this graph, and Phase
|
|
4
|
+
* Phase 1 narrows its Explore fan-out with this graph, and Phase 5 refreshes it
|
|
5
5
|
* instead of hand-writing architecture.md from a single task's window. The
|
|
6
6
|
* design follows graphify (Graphify-Labs/graphify): AST-quality extraction with
|
|
7
7
|
* zero LLM cost, one graph file, an incremental manifest gate, god-node hubs,
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file _run-paths.mjs - the single resolver for pipeline run-state paths.
|
|
3
|
+
*
|
|
4
|
+
* A run's files live under the log root in ONE of two layouts:
|
|
5
|
+
*
|
|
6
|
+
* nested <root>/<project>/<taskId>/ documented canonical; what
|
|
7
|
+
* agent-state.json, purge.sh's
|
|
8
|
+
* per-project .counter and
|
|
9
|
+
* prune-logs.sh reason about
|
|
10
|
+
* flat <root>/<taskId>/ what phase-tracker.sh has always
|
|
11
|
+
* written tracker-state.json to
|
|
12
|
+
*
|
|
13
|
+
* Both are real and both are populated, so every reader has to accept both.
|
|
14
|
+
* Before this module three callers hand-rolled their own candidate list -
|
|
15
|
+
* run-aggregator.mjs, render-cost-summary.sh and render-agent-log-cost.sh -
|
|
16
|
+
* and the single-level `<root>/*\/` globs elsewhere saw only one layout, so a
|
|
17
|
+
* run could be counted twice or missed entirely depending on which glob ran.
|
|
18
|
+
*
|
|
19
|
+
* This module does NOT change where anything is written. Measured on a real
|
|
20
|
+
* install, 90 of 103 runs are flat and every tracker-state.json is, so moving
|
|
21
|
+
* the writers would relocate the majority layout to satisfy a document. READS
|
|
22
|
+
* accept both, newest wins, and a taskId present in both layouts is ONE run,
|
|
23
|
+
* not two. Relocation is opt-in and explicit: `migrate-state.mjs --relocate`.
|
|
24
|
+
*
|
|
25
|
+
* Zero runtime dependencies (ADR-0004). Shell twin: pipeline/lib/run-paths.sh -
|
|
26
|
+
* the two must agree, and smoke-run-path-canonical.sh asserts that they do.
|
|
27
|
+
*
|
|
28
|
+
* @module pipeline/scripts/_run-paths
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Files that mark a directory as a run directory rather than a project
|
|
37
|
+
* directory. A project directory holds run directories; a run directory holds
|
|
38
|
+
* at least one of these.
|
|
39
|
+
*/
|
|
40
|
+
export const RUN_MARKERS = ["agent-state.json", "tracker-state.json", "agent-log.md"];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Depth-1 names under the log root that are namespaces, not runs and not
|
|
44
|
+
* projects. They are skipped by listRuns so a namespace never surfaces as a
|
|
45
|
+
* phantom run with no phase.
|
|
46
|
+
*/
|
|
47
|
+
export const RESERVED_DIRS = new Set([
|
|
48
|
+
"review-watch",
|
|
49
|
+
"jira-backups",
|
|
50
|
+
"shadow-git",
|
|
51
|
+
"_analysis-jira",
|
|
52
|
+
"prompts",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The log root. LOGS_ROOT overrides it, which is what the smokes use so they
|
|
57
|
+
* never touch a real run.
|
|
58
|
+
*
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function logsRoot() {
|
|
62
|
+
return process.env.LOGS_ROOT || join(homedir(), ".claude", "logs", "multi-agent");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The path a NEW run's files should be written to.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} taskId
|
|
69
|
+
* @param {string|null|undefined} project omitted only when the project is
|
|
70
|
+
* genuinely unknown, which yields the flat path rather than inventing a slug
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function canonicalRunDir(taskId, project) {
|
|
74
|
+
const root = logsRoot();
|
|
75
|
+
return project ? join(root, project, taskId) : join(root, taskId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Phase 4 removes the worktree and salvages the run's files into an
|
|
80
|
+
* `artifacts/` subdirectory of the same run directory. A finished run therefore
|
|
81
|
+
* keeps its state one level deeper, and a reader that only looks at the top
|
|
82
|
+
* level reports a shipped task as having no state at all.
|
|
83
|
+
*/
|
|
84
|
+
export const ARTIFACTS_SUBDIR = "artifacts";
|
|
85
|
+
|
|
86
|
+
function isRunDir(dir) {
|
|
87
|
+
if (RUN_MARKERS.some((m) => existsSync(join(dir, m)))) return true;
|
|
88
|
+
return RUN_MARKERS.some((m) => existsSync(join(dir, ARTIFACTS_SUBDIR, m)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Newest marker mtime in WHOLE SECONDS.
|
|
93
|
+
*
|
|
94
|
+
* Seconds, not milliseconds, because the shell twin can only read seconds
|
|
95
|
+
* (`stat -c %Y` / `stat -f %m`). Comparing at different precisions made the two
|
|
96
|
+
* implementations pick different winners for a run written to both layouts
|
|
97
|
+
* within the same second - which is the common case, since both writes happen
|
|
98
|
+
* in one Phase 0.
|
|
99
|
+
*/
|
|
100
|
+
function newestMarkerMtime(dir) {
|
|
101
|
+
let newest = 0;
|
|
102
|
+
for (const base of [dir, join(dir, ARTIFACTS_SUBDIR)]) {
|
|
103
|
+
for (const m of RUN_MARKERS) {
|
|
104
|
+
try {
|
|
105
|
+
const t = Math.floor(statSync(join(base, m)).mtimeMs / 1000);
|
|
106
|
+
if (t > newest) newest = t;
|
|
107
|
+
} catch {
|
|
108
|
+
// Marker absent or unreadable - not every run has all three.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return newest;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Total order over candidate directories for one task id. Negative when `a`
|
|
117
|
+
* should win.
|
|
118
|
+
*
|
|
119
|
+
* mtime alone is not an order: two directories written in the same second tie,
|
|
120
|
+
* and both implementations then fell back to their own traversal order and
|
|
121
|
+
* disagreed. The tail of this comparator exists so the answer is defined:
|
|
122
|
+
* richer record first (agent-state.json is what every reader actually wants),
|
|
123
|
+
* then the documented nested layout, then the path itself.
|
|
124
|
+
*/
|
|
125
|
+
function compareCandidates(a, b) {
|
|
126
|
+
// A bridge and its target hold the same bytes, so neither is "newer". Prefer
|
|
127
|
+
// the real directory: a reader that reports a path should report the one the
|
|
128
|
+
// file actually lives in.
|
|
129
|
+
const aBridge = isBridgeDir(a) ? 1 : 0;
|
|
130
|
+
const bBridge = isBridgeDir(b) ? 1 : 0;
|
|
131
|
+
if (aBridge !== bBridge) return aBridge - bBridge;
|
|
132
|
+
const byTime = newestMarkerMtime(b) - newestMarkerMtime(a);
|
|
133
|
+
if (byTime !== 0) return byTime;
|
|
134
|
+
const aState = existsSync(join(a, "agent-state.json")) ? 1 : 0;
|
|
135
|
+
const bState = existsSync(join(b, "agent-state.json")) ? 1 : 0;
|
|
136
|
+
if (aState !== bState) return bState - aState;
|
|
137
|
+
const aNested = a.split("/").length;
|
|
138
|
+
const bNested = b.split("/").length;
|
|
139
|
+
if (aNested !== bNested) return bNested - aNested;
|
|
140
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Every directory a run with this id could live in, most-specific first.
|
|
145
|
+
* Existence is NOT checked - this is the candidate list; resolveRunDir picks.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} taskId
|
|
148
|
+
* @param {string|null|undefined} project
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
export function runDirCandidates(taskId, project) {
|
|
152
|
+
const root = logsRoot();
|
|
153
|
+
const out = [];
|
|
154
|
+
if (project) out.push(join(root, project, taskId));
|
|
155
|
+
out.push(join(root, taskId));
|
|
156
|
+
if (!project) {
|
|
157
|
+
// No project given: the run may still be nested under one. Scan depth-1
|
|
158
|
+
// directories for a child with this id rather than failing the lookup.
|
|
159
|
+
let entries;
|
|
160
|
+
try {
|
|
161
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
162
|
+
} catch {
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
for (const e of entries) {
|
|
166
|
+
if (!e.isDirectory() || RESERVED_DIRS.has(e.name) || e.name === taskId) continue;
|
|
167
|
+
const cand = join(root, e.name, taskId);
|
|
168
|
+
if (isRunDir(cand)) out.push(cand);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The directory holding this run's files, or null when the run is unknown.
|
|
176
|
+
* When a taskId exists in more than one layout the most recently written one
|
|
177
|
+
* wins, so a resumed run is not read from its stale twin.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} taskId
|
|
180
|
+
* @param {string|null|undefined} [project]
|
|
181
|
+
* @returns {string|null}
|
|
182
|
+
*/
|
|
183
|
+
export function resolveRunDir(taskId, project) {
|
|
184
|
+
const present = runDirCandidates(taskId, project).filter(isRunDir);
|
|
185
|
+
if (!present.length) return null;
|
|
186
|
+
if (present.length === 1) return present[0];
|
|
187
|
+
return present.slice().sort(compareCandidates)[0];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The spellings one task id has been written under, in preference order.
|
|
192
|
+
*
|
|
193
|
+
* `#316` reaches the pipeline from a GitHub issue reference, `316` from the
|
|
194
|
+
* bare-number input class, and `task-316` from an older directory convention.
|
|
195
|
+
* Callers used to inline this list; centralising it is the point of this
|
|
196
|
+
* module, and dropping any spelling would silently stop resolving old runs.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} id
|
|
199
|
+
* @returns {string[]} unique, order-preserving
|
|
200
|
+
*/
|
|
201
|
+
export function taskIdVariants(id) {
|
|
202
|
+
const raw = String(id);
|
|
203
|
+
const bare = raw.replace(/^#/, "");
|
|
204
|
+
return [...new Set([raw, bare, `task-${bare}`])];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* resolveRunDir over every spelling of the id, first hit wins.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} taskId
|
|
211
|
+
* @param {string|null|undefined} [project]
|
|
212
|
+
* @returns {string|null}
|
|
213
|
+
*/
|
|
214
|
+
export function resolveRunDirAny(taskId, project) {
|
|
215
|
+
for (const v of taskIdVariants(taskId)) {
|
|
216
|
+
const dir = resolveRunDir(v, project);
|
|
217
|
+
if (dir) return dir;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The full path to one of a run's files, or null when the run is unknown.
|
|
224
|
+
* Every spelling of the id is tried.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} taskId
|
|
227
|
+
* @param {string} filename e.g. "agent-state.json"
|
|
228
|
+
* @param {string|null|undefined} [project]
|
|
229
|
+
* @returns {string|null}
|
|
230
|
+
*/
|
|
231
|
+
export function resolveRunFile(taskId, filename, project) {
|
|
232
|
+
for (const v of taskIdVariants(taskId)) {
|
|
233
|
+
const dir = resolveRunDir(v, project);
|
|
234
|
+
if (!dir) continue;
|
|
235
|
+
// Top level first, then the salvaged copy Phase 4 leaves behind.
|
|
236
|
+
for (const base of [dir, join(dir, ARTIFACTS_SUBDIR)]) {
|
|
237
|
+
const p = join(base, filename);
|
|
238
|
+
if (existsSync(p)) return p;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* True when two candidate directories are two views of ONE record rather than
|
|
246
|
+
* two copies of it.
|
|
247
|
+
*
|
|
248
|
+
* Some nested run directories are symlink bridges into the flat copy - a
|
|
249
|
+
* previous attempt to reconcile the two layouts. Counting those as duplicates
|
|
250
|
+
* overstates the drift (21 reported, 18 real on the install this was measured
|
|
251
|
+
* on) and makes "which one is newer" a question about a link's own mtime.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} a
|
|
254
|
+
* @param {string} b
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
function isSameRecord(a, b) {
|
|
258
|
+
for (const m of RUN_MARKERS) {
|
|
259
|
+
try {
|
|
260
|
+
if (realpathSync(join(a, m)) === realpathSync(join(b, m))) return true;
|
|
261
|
+
} catch {
|
|
262
|
+
// Marker missing on one side - try the next one.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* True when any marker in this directory is itself a symlink.
|
|
270
|
+
*
|
|
271
|
+
* lstat on the LEAF, not a realpath comparison: realpath also resolves parent
|
|
272
|
+
* directories, and on macOS a temp tree under /var is reached through a symlink
|
|
273
|
+
* to /private/var, so realpath(x) !== x is true for every path there. That made
|
|
274
|
+
* both sides of a pair look like bridges, the rule collapsed, and the two
|
|
275
|
+
* implementations picked different winners. The shell twin tests `[ -L ]`, and
|
|
276
|
+
* this has to mean the same thing.
|
|
277
|
+
*/
|
|
278
|
+
function isBridgeDir(dir) {
|
|
279
|
+
for (const m of RUN_MARKERS) {
|
|
280
|
+
try {
|
|
281
|
+
if (lstatSync(join(dir, m)).isSymbolicLink()) return true;
|
|
282
|
+
} catch {
|
|
283
|
+
// Missing marker is not a bridge.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readProjectHint(dir) {
|
|
290
|
+
// agent-state.json v2.1.0 mirrors projects[0] onto scalars; older revisions
|
|
291
|
+
// carry `project` as a string or an object with a name. Take whichever is a
|
|
292
|
+
// non-empty string and do not guess beyond that.
|
|
293
|
+
const statePath = existsSync(join(dir, "agent-state.json"))
|
|
294
|
+
? join(dir, "agent-state.json")
|
|
295
|
+
: join(dir, ARTIFACTS_SUBDIR, "agent-state.json");
|
|
296
|
+
try {
|
|
297
|
+
const s = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
298
|
+
const cands = [s.projectSlug, s.project?.name, s.project, s.projects?.[0]?.name];
|
|
299
|
+
for (const c of cands) if (typeof c === "string" && c.trim()) return c.trim();
|
|
300
|
+
} catch {
|
|
301
|
+
// No state file, or malformed - the layout still tells us what it can.
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Every run under the log root, both layouts, deduplicated by task id.
|
|
308
|
+
*
|
|
309
|
+
* A task id present in both layouts collapses to one entry - chosen by
|
|
310
|
+
* compareCandidates - with `duplicateOf` naming the other so a caller can
|
|
311
|
+
* report the drift instead of silently hiding it.
|
|
312
|
+
*
|
|
313
|
+
* `project` is derived from the LAYOUT alone, so the shell twin can produce the
|
|
314
|
+
* same four columns without parsing JSON (it has no jq guarantee). Anything
|
|
315
|
+
* read out of the state file is `projectHint`, a JS-only enrichment that
|
|
316
|
+
* runs-index.mjs uses and the cross-implementation contract does not cover.
|
|
317
|
+
*
|
|
318
|
+
* Ordering is byte-wise, not locale-aware, for the same reason: `sort` in the
|
|
319
|
+
* twin is byte-wise, and a collation difference is a diff nobody can act on.
|
|
320
|
+
*
|
|
321
|
+
* @returns {{taskId:string,project:string|null,dir:string,layout:"nested"|"flat",duplicateOf:string|null,projectHint:string|null}[]}
|
|
322
|
+
*/
|
|
323
|
+
export function listRuns() {
|
|
324
|
+
const root = logsRoot();
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
328
|
+
} catch {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @type {Map<string, {taskId:string,project:string|null,dir:string,layout:"nested"|"flat",duplicateOf:string|null}>} */
|
|
333
|
+
const byId = new Map();
|
|
334
|
+
|
|
335
|
+
const offer = (taskId, project, dir, layout) => {
|
|
336
|
+
const prev = byId.get(taskId);
|
|
337
|
+
if (!prev) {
|
|
338
|
+
byId.set(taskId, { taskId, project, dir, layout, duplicateOf: null });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const keepNew = compareCandidates(dir, prev.dir) < 0;
|
|
342
|
+
const winner = keepNew ? { taskId, project, dir, layout } : prev;
|
|
343
|
+
const loserDir = keepNew ? prev.dir : dir;
|
|
344
|
+
// A symlink bridge is one record seen twice, not drift worth reporting.
|
|
345
|
+
const dup = isSameRecord(dir, prev.dir) ? (prev.duplicateOf ?? null) : loserDir;
|
|
346
|
+
byId.set(taskId, { ...winner, duplicateOf: dup });
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
for (const e of entries) {
|
|
350
|
+
if (!e.isDirectory() || RESERVED_DIRS.has(e.name)) continue;
|
|
351
|
+
const dir = join(root, e.name);
|
|
352
|
+
if (isRunDir(dir)) {
|
|
353
|
+
offer(e.name, null, dir, "flat");
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
let kids;
|
|
357
|
+
try {
|
|
358
|
+
kids = readdirSync(dir, { withFileTypes: true });
|
|
359
|
+
} catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
for (const k of kids) {
|
|
363
|
+
if (!k.isDirectory()) continue;
|
|
364
|
+
const kd = join(dir, k.name);
|
|
365
|
+
if (isRunDir(kd)) offer(k.name, e.name, kd, "nested");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return [...byId.values()]
|
|
370
|
+
.map((r) => ({ ...r, projectHint: r.project ?? readProjectHint(r.dir) }))
|
|
371
|
+
.sort((a, b) => (a.taskId < b.taskId ? -1 : a.taskId > b.taskId ? 1 : 0));
|
|
372
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
#
|
|
21
21
|
# Usage (from any script in pipeline/scripts/):
|
|
22
22
|
# . "$(dirname "${BASH_SOURCE[0]}")/_smoke-root.sh"
|
|
23
|
-
# grep -q needle "$MA_REFS/phases/phase-
|
|
23
|
+
# grep -q needle "$MA_REFS/phases/phase-3-review.md"
|
|
24
24
|
#
|
|
25
25
|
# Exports:
|
|
26
26
|
# MA_LAYOUT "repo" | "install"
|