@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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fatal.mjs - one line instead of a stack dump, and whatever was held gets
|
|
3
|
+
* released.
|
|
4
|
+
*
|
|
5
|
+
* Node's default for an uncaught throw or a rejected promise nobody awaited is
|
|
6
|
+
* a stack trace on stderr and exit 1. That is the right default for a library
|
|
7
|
+
* and the wrong one here for two reasons.
|
|
8
|
+
*
|
|
9
|
+
* The first is legibility. Every script in this tree reports its own failures
|
|
10
|
+
* as `name: what went wrong`, one line, and a caller - a shell gate, a phase
|
|
11
|
+
* step, a person reading a run log - is written against that shape. A stack
|
|
12
|
+
* dump in the middle of it reads as a crash of the pipeline rather than as a
|
|
13
|
+
* failure of one step, and the actual message sits four frames down.
|
|
14
|
+
*
|
|
15
|
+
* The second is state, and it is the one that costs something. A script that
|
|
16
|
+
* dies between acquiring an advisory lock and releasing it leaves the lock on
|
|
17
|
+
* disk. The next writer then waits out the full acquire window before deciding
|
|
18
|
+
* the holder is stale - and if it judges wrong it takes a lock a LIVE writer
|
|
19
|
+
* holds. `cleanup` exists for exactly that: the handler releases before it
|
|
20
|
+
* exits, so the abnormal path leaves the same state behind as the normal one.
|
|
21
|
+
*
|
|
22
|
+
* What this does NOT do is swallow anything. Every path still exits non-zero,
|
|
23
|
+
* and the message still names the error; only the stack goes, and only when
|
|
24
|
+
* the error carries a message worth reading on its own.
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* import { runMain } from "../lib/fatal.mjs";
|
|
28
|
+
* runMain("write-state", main, { cleanup: releaseLock });
|
|
29
|
+
*
|
|
30
|
+
* @module pipeline/lib/fatal
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Guards against a cleanup that runs twice when a throw follows a rejection. */
|
|
34
|
+
let cleanedUp = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run `cleanup` at most once, and never let it become the failure it is
|
|
38
|
+
* cleaning up after: a cleanup that throws inside a fatal handler replaces the
|
|
39
|
+
* original error with its own, which is how a lock bug ends up reported as a
|
|
40
|
+
* permissions bug.
|
|
41
|
+
*
|
|
42
|
+
* @param {(() => void)|undefined} cleanup
|
|
43
|
+
*/
|
|
44
|
+
function runCleanup(cleanup) {
|
|
45
|
+
if (cleanedUp || typeof cleanup !== "function") return;
|
|
46
|
+
cleanedUp = true;
|
|
47
|
+
try {
|
|
48
|
+
cleanup();
|
|
49
|
+
} catch {
|
|
50
|
+
// Deliberately silent. The caller is already exiting with the real error.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The message a human should see. An Error with a message prints the message;
|
|
56
|
+
* anything else (a thrown string, a rejected non-Error) prints its own
|
|
57
|
+
* stringification, because dropping it would leave the line with nothing in it.
|
|
58
|
+
*
|
|
59
|
+
* @param {unknown} err
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function describe(err) {
|
|
63
|
+
if (err instanceof Error && err.message) return err.message;
|
|
64
|
+
if (typeof err === "string" && err) return err;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(err);
|
|
67
|
+
} catch {
|
|
68
|
+
return String(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Catch what escapes: a rejected promise nobody awaited, and a throw from a
|
|
74
|
+
* callback that no try/catch surrounds. Both are invisible to a try/catch
|
|
75
|
+
* around main(), which is why wrapping main() alone is not enough.
|
|
76
|
+
*
|
|
77
|
+
* MULTI_AGENT_FATAL_STACK=1 restores the stack for debugging. It is off by
|
|
78
|
+
* default because the line is for the person running the pipeline, not for the
|
|
79
|
+
* person maintaining it.
|
|
80
|
+
*
|
|
81
|
+
* @param {{name:string, cleanup?:()=>void, code?:number}} opts
|
|
82
|
+
*/
|
|
83
|
+
export function installFatalHandlers({ name, cleanup, code = 1 }) {
|
|
84
|
+
const report = (kind, err) => {
|
|
85
|
+
runCleanup(cleanup);
|
|
86
|
+
// `runs-index.mjs --json | head` is not a failure: head closes the pipe and
|
|
87
|
+
// the next write raises EPIPE. Reporting it as a fatal would turn the most
|
|
88
|
+
// ordinary way of looking at a large output into a red line and a non-zero
|
|
89
|
+
// exit, and the reader that went away is not listening to the complaint
|
|
90
|
+
// anyway. Quiet, zero, the way every other CLI treats it.
|
|
91
|
+
if (err && err.code === "EPIPE") process.exit(0);
|
|
92
|
+
process.stderr.write(`${name}: ${kind} - ${describe(err)}\n`);
|
|
93
|
+
if (process.env.MULTI_AGENT_FATAL_STACK === "1" && err instanceof Error && err.stack) {
|
|
94
|
+
process.stderr.write(`${err.stack}\n`);
|
|
95
|
+
}
|
|
96
|
+
// exit(), not exitCode: the event loop may hold a listener that would keep
|
|
97
|
+
// the process alive after the failure, and a script that has already
|
|
98
|
+
// reported a fatal must not go on to do more work.
|
|
99
|
+
process.exit(code);
|
|
100
|
+
};
|
|
101
|
+
process.on("unhandledRejection", (err) => report("unhandled rejection", err));
|
|
102
|
+
process.on("uncaughtException", (err) => report("uncaught exception", err));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Install the handlers and run main, sync or async, with the same contract for
|
|
107
|
+
* both. A successful run is untouched: no exit call, so `process.exitCode` set
|
|
108
|
+
* inside main survives, and stdout drains the way it would have.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} name script name as it should appear in the message
|
|
111
|
+
* @param {() => unknown} main
|
|
112
|
+
* @param {{cleanup?:()=>void, code?:number}} [opts]
|
|
113
|
+
* @returns {Promise<void>}
|
|
114
|
+
*/
|
|
115
|
+
export async function runMain(name, main, opts = {}) {
|
|
116
|
+
const { cleanup, code = 1 } = opts;
|
|
117
|
+
installFatalHandlers({ name, cleanup, code });
|
|
118
|
+
try {
|
|
119
|
+
await main();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
runCleanup(cleanup);
|
|
122
|
+
if (err && err.code === "EPIPE") process.exit(0);
|
|
123
|
+
process.stderr.write(`${name}: ${describe(err)}\n`);
|
|
124
|
+
if (process.env.MULTI_AGENT_FATAL_STACK === "1" && err instanceof Error && err.stack) {
|
|
125
|
+
process.stderr.write(`${err.stack}\n`);
|
|
126
|
+
}
|
|
127
|
+
process.exit(code);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
# 6 not configured (prefs.global.hosts.fortify empty, or an instance-id-only
|
|
50
50
|
# lookup with no prefs.global.fortify.versionIds to search)
|
|
51
51
|
#
|
|
52
|
-
# Phase
|
|
52
|
+
# Phase 3 review gate contract:
|
|
53
53
|
# Critical > 0 → blocking=true, reason="critical-findings"
|
|
54
54
|
# High > 0 → blocking=false, reason="high-findings-warning"
|
|
55
55
|
# else → blocking=false, reason="clean"
|
|
@@ -18,6 +18,24 @@
|
|
|
18
18
|
|
|
19
19
|
set -euo pipefail
|
|
20
20
|
|
|
21
|
+
# jq is not optional on this path. Without the guard below a missing binary
|
|
22
|
+
# renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
|
|
23
|
+
for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
|
|
24
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
|
|
25
|
+
"$HOME/.claude/lib/require-jq.sh" \
|
|
26
|
+
"$HOME/.copilot/lib/require-jq.sh" \
|
|
27
|
+
"$HOME/.codex/lib/require-jq.sh"; do
|
|
28
|
+
[ -f "$_rq" ] || continue
|
|
29
|
+
# shellcheck source=/dev/null
|
|
30
|
+
. "$_rq" && break
|
|
31
|
+
done
|
|
32
|
+
unset _rq
|
|
33
|
+
if ! command -v ma_require_jq >/dev/null 2>&1; then
|
|
34
|
+
# The helper itself is missing, which is an install problem, not a jq one.
|
|
35
|
+
ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
|
|
36
|
+
fi
|
|
37
|
+
ma_require_jq "refresh the Figma MCP token" || exit 3
|
|
38
|
+
|
|
21
39
|
PREFS_FILE="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
|
|
22
40
|
TOKEN_ENDPOINT="https://api.figma.com/v1/oauth/token"
|
|
23
41
|
|
|
@@ -39,6 +39,24 @@
|
|
|
39
39
|
|
|
40
40
|
set -euo pipefail
|
|
41
41
|
|
|
42
|
+
# jq is not optional on this path. Without the guard below a missing binary
|
|
43
|
+
# renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
|
|
44
|
+
for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
|
|
45
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
|
|
46
|
+
"$HOME/.claude/lib/require-jq.sh" \
|
|
47
|
+
"$HOME/.copilot/lib/require-jq.sh" \
|
|
48
|
+
"$HOME/.codex/lib/require-jq.sh"; do
|
|
49
|
+
[ -f "$_rq" ] || continue
|
|
50
|
+
# shellcheck source=/dev/null
|
|
51
|
+
. "$_rq" && break
|
|
52
|
+
done
|
|
53
|
+
unset _rq
|
|
54
|
+
if ! command -v ma_require_jq >/dev/null 2>&1; then
|
|
55
|
+
# The helper itself is missing, which is an install problem, not a jq one.
|
|
56
|
+
ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
|
|
57
|
+
fi
|
|
58
|
+
ma_require_jq "fetch a Figma screenshot" || exit 3
|
|
59
|
+
|
|
42
60
|
# --- Globals ----------------------------------------------------------------
|
|
43
61
|
|
|
44
62
|
# Locate the resolver with an existence check, not a `.`-chain: sourcing a missing file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* invoked-directly.mjs - "was this file run, or imported", answered correctly.
|
|
3
|
+
*
|
|
4
|
+
* The idiom this replaces was in nine files:
|
|
5
|
+
*
|
|
6
|
+
* if (import.meta.url === `file://${process.argv[1]}`) main();
|
|
7
|
+
*
|
|
8
|
+
* It compares a RESOLVED, percent-encoded URL against a raw path, and it is
|
|
9
|
+
* false in three situations that are not exotic:
|
|
10
|
+
*
|
|
11
|
+
* - a symlink. `install --link` symlinks whole directories into ~/.claude, so
|
|
12
|
+
* every script a linked install runs is reached through one;
|
|
13
|
+
* - a path that resolves elsewhere. On macOS /var is a symlink to /private/var,
|
|
14
|
+
* which is where mktemp puts everything;
|
|
15
|
+
* - a path containing a space, or any character a URL escapes.
|
|
16
|
+
*
|
|
17
|
+
* In all three the guard is false and the script does nothing, silently, exit 0.
|
|
18
|
+
* On `outbound-gate.mjs` that was a security hole rather than an inconvenience:
|
|
19
|
+
* the callers read exit 0 as "this text is clean", so a body carrying a token
|
|
20
|
+
* would have been published unscanned by any install that used --link.
|
|
21
|
+
*
|
|
22
|
+
* @module pipeline/lib/invoked-directly
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { realpathSync } from "node:fs";
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True when `metaUrl` names the file Node was asked to run.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} metaUrl the caller's own `import.meta.url`
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function invokedDirectly(metaUrl) {
|
|
35
|
+
const entry = process.argv[1];
|
|
36
|
+
if (!entry) return false;
|
|
37
|
+
try {
|
|
38
|
+
return metaUrl === pathToFileURL(realpathSync(entry)).href;
|
|
39
|
+
} catch {
|
|
40
|
+
// An argv[1] that cannot be resolved is not this file.
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -40,6 +40,47 @@
|
|
|
40
40
|
|
|
41
41
|
set -euo pipefail
|
|
42
42
|
|
|
43
|
+
# jq is not optional on this path. Without the guard below a missing binary
|
|
44
|
+
# renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
|
|
45
|
+
for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
|
|
46
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
|
|
47
|
+
"$HOME/.claude/lib/require-jq.sh" \
|
|
48
|
+
"$HOME/.copilot/lib/require-jq.sh" \
|
|
49
|
+
"$HOME/.codex/lib/require-jq.sh"; do
|
|
50
|
+
[ -f "$_rq" ] || continue
|
|
51
|
+
# shellcheck source=/dev/null
|
|
52
|
+
. "$_rq" && break
|
|
53
|
+
done
|
|
54
|
+
unset _rq
|
|
55
|
+
if ! command -v ma_require_jq >/dev/null 2>&1; then
|
|
56
|
+
# The helper itself is missing, which is an install problem, not a jq one.
|
|
57
|
+
ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
|
|
58
|
+
fi
|
|
59
|
+
ma_require_jq "build the Jira comment" || exit 3
|
|
60
|
+
|
|
61
|
+
# Outbound leak gate. Every byte below is composed at runtime out of command
|
|
62
|
+
# output, error text and file excerpts, any of which can carry a token that was
|
|
63
|
+
# on this machine a second earlier - and once it is in a comment it is in
|
|
64
|
+
# someone else's database. The repo's own leak scanner looks at FILES IN THE
|
|
65
|
+
# REPO and never sees this text. See lib/outbound-gate.mjs.
|
|
66
|
+
ma_outbound_gate() {
|
|
67
|
+
local body_file="$1" og=""
|
|
68
|
+
for c in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/outbound-gate.mjs" \
|
|
69
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/outbound-gate.mjs" \
|
|
70
|
+
"$HOME/.claude/lib/outbound-gate.mjs" \
|
|
71
|
+
"$HOME/.copilot/lib/outbound-gate.mjs" \
|
|
72
|
+
"$HOME/.codex/lib/outbound-gate.mjs"; do
|
|
73
|
+
[ -f "$c" ] && { og="$c"; break; }
|
|
74
|
+
done
|
|
75
|
+
# Missing gate is NOT an open door: refusing to publish beats publishing
|
|
76
|
+
# unchecked, and the only way this file is absent is a broken install.
|
|
77
|
+
if [ -z "$og" ]; then
|
|
78
|
+
echo "outbound-gate.mjs not found - refusing to publish unchecked text." >&2
|
|
79
|
+
return 7
|
|
80
|
+
fi
|
|
81
|
+
node "$og" --file "$body_file"
|
|
82
|
+
}
|
|
83
|
+
|
|
43
84
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
44
85
|
PREFS="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
|
|
45
86
|
CURL="${JIRA_PUBLISH_CURL:-curl}"
|
|
@@ -73,6 +114,7 @@ done
|
|
|
73
114
|
[ -n "$ISSUE" ] || die "usage: jira-publish.sh --issue KEY --body-file FILE [--target comment|description]"
|
|
74
115
|
[ -n "$BODY_FILE" ] || die "usage: jira-publish.sh --issue KEY --body-file FILE [--target comment|description]"
|
|
75
116
|
[ -f "$BODY_FILE" ] || die "body file not found: $BODY_FILE"
|
|
117
|
+
ma_outbound_gate "$BODY_FILE" || die "outbound gate refused this comment; nothing was posted"
|
|
76
118
|
case "$TARGET" in comment|description) ;; *) die "--target must be comment or description, got: $TARGET" ;; esac
|
|
77
119
|
case "$MODE" in append|replace) ;; *) die "--mode must be append or replace, got: $MODE" ;; esac
|
|
78
120
|
|
|
@@ -35,6 +35,7 @@ import argparse
|
|
|
35
35
|
import html
|
|
36
36
|
import io
|
|
37
37
|
import json
|
|
38
|
+
import tempfile
|
|
38
39
|
import mimetypes
|
|
39
40
|
import os
|
|
40
41
|
import re
|
|
@@ -962,6 +963,10 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
962
963
|
sys.stderr.write(json.dumps(envelope, indent=2, ensure_ascii=False) + "\n")
|
|
963
964
|
return 0
|
|
964
965
|
|
|
966
|
+
# Every byte below this line leaves the machine. The dry-run branch above
|
|
967
|
+
# returns before it, which is why the gate sits here and not at the top.
|
|
968
|
+
outbound_gate(md_text)
|
|
969
|
+
|
|
965
970
|
auth = resolve_auth()
|
|
966
971
|
warnings = list(result.warnings)
|
|
967
972
|
|
|
@@ -1194,6 +1199,48 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1194
1199
|
return parser
|
|
1195
1200
|
|
|
1196
1201
|
|
|
1202
|
+
def outbound_gate(text: str) -> None:
|
|
1203
|
+
"""Refuse to publish text the leak gate has not cleared.
|
|
1204
|
+
|
|
1205
|
+
This is the choke point for the Confluence half of the pipeline's outbound
|
|
1206
|
+
surface: a page is created or updated only from here, and everything above
|
|
1207
|
+
is rendering. The gate itself is the same one the Jira and PR paths use -
|
|
1208
|
+
one set of patterns, one verdict, rather than a second copy that drifts.
|
|
1209
|
+
|
|
1210
|
+
A missing gate file REFUSES. The only way it is absent is a broken install,
|
|
1211
|
+
and publishing unchecked because the check could not be loaded is the single
|
|
1212
|
+
failure this exists to prevent.
|
|
1213
|
+
"""
|
|
1214
|
+
candidates = [
|
|
1215
|
+
Path(__file__).resolve().parent / "outbound-gate.mjs",
|
|
1216
|
+
Path.home() / ".claude" / "lib" / "outbound-gate.mjs",
|
|
1217
|
+
Path.home() / ".copilot" / "lib" / "outbound-gate.mjs",
|
|
1218
|
+
Path.home() / ".codex" / "lib" / "outbound-gate.mjs",
|
|
1219
|
+
]
|
|
1220
|
+
gate = next((c for c in candidates if c.is_file()), None)
|
|
1221
|
+
if gate is None:
|
|
1222
|
+
sys.stderr.write("outbound-gate.mjs not found - refusing to publish unchecked text.\n")
|
|
1223
|
+
raise SystemExit(2)
|
|
1224
|
+
|
|
1225
|
+
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as fh:
|
|
1226
|
+
fh.write(text)
|
|
1227
|
+
tmp = fh.name
|
|
1228
|
+
try:
|
|
1229
|
+
proc = subprocess.run(
|
|
1230
|
+
["node", str(gate), "--file", tmp],
|
|
1231
|
+
capture_output=True,
|
|
1232
|
+
text=True,
|
|
1233
|
+
check=False,
|
|
1234
|
+
)
|
|
1235
|
+
finally:
|
|
1236
|
+
os.unlink(tmp)
|
|
1237
|
+
if proc.returncode != 0:
|
|
1238
|
+
sys.stderr.write(proc.stdout)
|
|
1239
|
+
sys.stderr.write(proc.stderr)
|
|
1240
|
+
sys.stderr.write("outbound gate refused this page; nothing was published.\n")
|
|
1241
|
+
raise SystemExit(2)
|
|
1242
|
+
|
|
1243
|
+
|
|
1197
1244
|
def main(argv: list[str] | None = None) -> int:
|
|
1198
1245
|
parser = build_parser()
|
|
1199
1246
|
args = parser.parse_args(argv)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# model-rung.sh - read or flip the top model rung, and keep cost pricing with it.
|
|
3
|
+
#
|
|
4
|
+
# Backs /multi-agent:model. Two preference keys move as ONE write:
|
|
5
|
+
# prefs.global.modelFallback.fableEnabled the rung itself
|
|
6
|
+
# prefs.global.costBudget.pricingModel what the ledger prices against
|
|
7
|
+
#
|
|
8
|
+
# They were always meant to move together - model-fallback.md says so in prose -
|
|
9
|
+
# but nothing enforced it, so a hand edit that flipped one left the estimate
|
|
10
|
+
# pricing every call at a rung that was not in play. With the rung off and
|
|
11
|
+
# pricing still `fable`, the budget ceiling trips early and triggers a downgrade
|
|
12
|
+
# nothing needed. One write, or none.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# model-rung.sh report current state (exit 0)
|
|
16
|
+
# model-rung.sh on enable the fable rung
|
|
17
|
+
# model-rung.sh off disable it
|
|
18
|
+
#
|
|
19
|
+
# Exit codes:
|
|
20
|
+
# 0 - reported or applied
|
|
21
|
+
# 2 - bad argument
|
|
22
|
+
# 3 - preferences file missing or unparseable (nothing written)
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
PREFS="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
|
|
27
|
+
ACTION="${1:-report}"
|
|
28
|
+
|
|
29
|
+
case "$ACTION" in
|
|
30
|
+
report | on | off) ;;
|
|
31
|
+
*)
|
|
32
|
+
echo "usage: model-rung.sh [on|off]" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
# A missing `jq` must not look like a missing setting. Without this the reads
|
|
38
|
+
# below return empty, the command reports the rung as already off, and a user
|
|
39
|
+
# who asked to turn it off is told it was never on.
|
|
40
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
41
|
+
echo "model-rung: jq not found - cannot read or write preferences (install: brew install jq)" >&2
|
|
42
|
+
exit 3
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ ! -f "$PREFS" ]; then
|
|
46
|
+
echo "model-rung: preferences not found at $PREFS - run /multi-agent:setup first" >&2
|
|
47
|
+
exit 3
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if ! jq -e . "$PREFS" >/dev/null 2>&1; then
|
|
51
|
+
echo "model-rung: $PREFS is not valid JSON - refusing to write over it" >&2
|
|
52
|
+
exit 3
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
CUR=$(jq -r '.global.modelFallback.fableEnabled // false' "$PREFS")
|
|
56
|
+
PRICING=$(jq -r '.global.costBudget.pricingModel // "fable"' "$PREFS")
|
|
57
|
+
|
|
58
|
+
host_note() {
|
|
59
|
+
# Which host this is decides whether the switch is a switch or a status line.
|
|
60
|
+
# Saying "enabled" on a host where the rung means something else entirely is
|
|
61
|
+
# the surprise this block exists to avoid.
|
|
62
|
+
case "${MULTI_AGENT_HOST:-claude}" in
|
|
63
|
+
claude)
|
|
64
|
+
echo "Claude Code: the fable rung is Fable 5. This switch is live here."
|
|
65
|
+
;;
|
|
66
|
+
copilot)
|
|
67
|
+
echo "Copilot CLI: Fable 5 is not offered; its personas never sat on this rung."
|
|
68
|
+
echo "This setting is recorded but changes nothing on this host."
|
|
69
|
+
;;
|
|
70
|
+
codex)
|
|
71
|
+
echo "Codex CLI: the fable rung means gpt-5.6 @ xhigh, a different model on a"
|
|
72
|
+
echo "different account. This switch deliberately does NOT touch it - a knob"
|
|
73
|
+
echo "named after an Anthropic model must not silently retune a Codex run."
|
|
74
|
+
;;
|
|
75
|
+
*)
|
|
76
|
+
echo "Unknown host '${MULTI_AGENT_HOST}'; reporting the stored value only."
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if [ "$ACTION" = "report" ]; then
|
|
82
|
+
echo "fable rung: $CUR"
|
|
83
|
+
echo "cost pricing: $PRICING"
|
|
84
|
+
if [ "$CUR" = "true" ] && [ "$PRICING" != "fable" ]; then
|
|
85
|
+
echo "MISMATCH: rung is on but the ledger prices against '$PRICING' - run 'model on' to realign"
|
|
86
|
+
elif [ "$CUR" != "true" ] && [ "$PRICING" = "fable" ]; then
|
|
87
|
+
echo "MISMATCH: rung is off but the ledger still prices against fable - this trips"
|
|
88
|
+
echo " the budget ceiling early. Run 'model off' to realign."
|
|
89
|
+
fi
|
|
90
|
+
echo ""
|
|
91
|
+
host_note
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if [ "$ACTION" = "on" ]; then
|
|
96
|
+
WANT=true
|
|
97
|
+
WANT_PRICING=fable
|
|
98
|
+
else
|
|
99
|
+
WANT=false
|
|
100
|
+
WANT_PRICING=opus
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
TMP=$(mktemp) || { echo "model-rung: mktemp failed" >&2; exit 3; }
|
|
104
|
+
trap 'rm -f "$TMP"' EXIT
|
|
105
|
+
|
|
106
|
+
# Both keys in one filter. A two-step write can leave the pair half-applied if
|
|
107
|
+
# the second step fails, which is the exact state this command exists to prevent.
|
|
108
|
+
if ! jq --argjson want "$WANT" --arg pricing "$WANT_PRICING" '
|
|
109
|
+
.global.modelFallback //= {}
|
|
110
|
+
| .global.modelFallback.fableEnabled = $want
|
|
111
|
+
| .global.costBudget //= {}
|
|
112
|
+
| .global.costBudget.pricingModel = $pricing
|
|
113
|
+
' "$PREFS" > "$TMP"; then
|
|
114
|
+
echo "model-rung: jq failed - preferences left untouched" >&2
|
|
115
|
+
exit 3
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if ! jq -e . "$TMP" >/dev/null 2>&1; then
|
|
119
|
+
echo "model-rung: produced invalid JSON - preferences left untouched" >&2
|
|
120
|
+
exit 3
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
mv "$TMP" "$PREFS"
|
|
124
|
+
trap - EXIT
|
|
125
|
+
|
|
126
|
+
echo "fable rung: $CUR -> $WANT"
|
|
127
|
+
echo "cost pricing: $PRICING -> $WANT_PRICING"
|
|
128
|
+
echo ""
|
|
129
|
+
|
|
130
|
+
if [ "$WANT" = "false" ]; then
|
|
131
|
+
# Said at the moment of the change rather than left in a doc: the panel
|
|
132
|
+
# shrinking is the part people discover later and read as a bug.
|
|
133
|
+
echo "Phase 3 reviewer panel: 3 models -> 2 (opus + sonnet)."
|
|
134
|
+
echo "Reviewer 1 lands on opus, which Reviewer 2 already holds, and dispatching"
|
|
135
|
+
echo "one model twice is not cross-model review. consensus.reviewerCount records 2."
|
|
136
|
+
echo "Triage also runs on opus, so it shares a model with Reviewer 1 - the Phase 3"
|
|
137
|
+
echo "Step 3 anonymisation requirement is not optional while the rung is off."
|
|
138
|
+
echo ""
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
host_note
|
|
142
|
+
exit 0
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* outbound-gate.mjs - nothing leaves this machine carrying a credential.
|
|
4
|
+
*
|
|
5
|
+
* The repo already has a leak scanner, and it looks at the wrong thing for
|
|
6
|
+
* this purpose: `smoke-personal-data.sh` audits the FILES IN THE REPO so the
|
|
7
|
+
* published pipeline stays generic. It never sees a Jira comment, a PR body or
|
|
8
|
+
* an issue update - text that a run composes at runtime out of command output,
|
|
9
|
+
* error messages and file excerpts, any of which can contain a token that was
|
|
10
|
+
* on the machine a second earlier.
|
|
11
|
+
*
|
|
12
|
+
* SCOPE, and the part the plan for this got wrong: corporate names are NOT
|
|
13
|
+
* checked here. A Jira comment on a corporate ticket legitimately contains the
|
|
14
|
+
* corporate project key, the repo name and the hostname - refusing those would
|
|
15
|
+
* make the gate fire on every correct publish, and a gate that cries wolf is
|
|
16
|
+
* turned off. Repo genericity and run-output hygiene are different jobs with
|
|
17
|
+
* different lists, and only the second one ships.
|
|
18
|
+
*
|
|
19
|
+
* WIRED INTO, and the honest limit: the three SHELL publishers -
|
|
20
|
+
* lib/jira-publish.sh (which is also how `channels` posts to Jira),
|
|
21
|
+
* lib/post-pr-review.sh (per-finding comments and the request-changes body),
|
|
22
|
+
* and scripts/update-issue-progress.sh. That is every publish this repo makes
|
|
23
|
+
* from a script.
|
|
24
|
+
*
|
|
25
|
+
* It is NOT every publish. `channels` also writes a Confluence page and a PR
|
|
26
|
+
* description, and those are composed and posted by the agent following the
|
|
27
|
+
* skill rather than by a script, so nothing in shell can intercept them. A
|
|
28
|
+
* gate that claimed four paths while covering three would be worse than one
|
|
29
|
+
* that covers three and says so.
|
|
30
|
+
*
|
|
31
|
+
* WHAT IT NEVER DOES: print the thing it found. A gate that echoes the secret
|
|
32
|
+
* into a terminal, a log and a CI transcript has moved it somewhere new.
|
|
33
|
+
* Findings are reported as rule name plus line number, nothing else.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* cat body.md | outbound-gate.mjs # exit 0 clean, 7 if blocked
|
|
37
|
+
* outbound-gate.mjs --file body.md
|
|
38
|
+
* cat body.md | outbound-gate.mjs --json
|
|
39
|
+
* cat body.md | outbound-gate.mjs --warn # report, never block
|
|
40
|
+
*
|
|
41
|
+
* Exit codes:
|
|
42
|
+
* 0 - nothing found
|
|
43
|
+
* 7 - at least one finding; the caller must not publish
|
|
44
|
+
* 2 - usage error (no input)
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { readFileSync } from "node:fs";
|
|
48
|
+
import { invokedDirectly } from "./invoked-directly.mjs";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Each rule is a SHAPE, not a dictionary. A list of known token prefixes goes
|
|
52
|
+
* stale the moment a provider adds one; a shape keeps working.
|
|
53
|
+
*
|
|
54
|
+
* `confidence` separates the two kinds of finding. A GitHub PAT has a fixed
|
|
55
|
+
* prefix and length and is never a false positive - that blocks. An email
|
|
56
|
+
* address is a shape that legitimate prose contains, so it is reported and the
|
|
57
|
+
* caller decides. Mixing them would mean either blocking valid publishes or
|
|
58
|
+
* letting credentials through, and there is no threshold that does both.
|
|
59
|
+
*/
|
|
60
|
+
export const RULES = [
|
|
61
|
+
// --- credentials: a match is a match -------------------------------------
|
|
62
|
+
{ name: "github-pat", certain: true, re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
|
|
63
|
+
{ name: "github-fine-grained-pat", certain: true, re: /\bgithub_pat_[A-Za-z0-9_]{50,}\b/g },
|
|
64
|
+
{ name: "slack-token", certain: true, re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
65
|
+
{ name: "aws-access-key-id", certain: true, re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
66
|
+
{ name: "google-api-key", certain: true, re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
67
|
+
{ name: "openai-key", certain: true, re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/g },
|
|
68
|
+
{ name: "anthropic-key", certain: true, re: /\bsk-ant-[A-Za-z0-9_-]{32,}\b/g },
|
|
69
|
+
{ name: "figma-token", certain: true, re: /\bfig[a-z]_[A-Za-z0-9_-]{20,}\b/g },
|
|
70
|
+
{ name: "npm-token", certain: true, re: /\bnpm_[A-Za-z0-9]{36}\b/g },
|
|
71
|
+
{
|
|
72
|
+
name: "jwt",
|
|
73
|
+
certain: true,
|
|
74
|
+
re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
75
|
+
},
|
|
76
|
+
{ name: "private-key-block", certain: true, re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
|
|
77
|
+
// A URL carrying credentials. This is the one that leaks by accident most
|
|
78
|
+
// often: a `git remote -v` pasted into a comment.
|
|
79
|
+
{
|
|
80
|
+
name: "url-with-credentials",
|
|
81
|
+
certain: true,
|
|
82
|
+
re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/g,
|
|
83
|
+
},
|
|
84
|
+
// `Authorization: Bearer <something long>` copied out of a curl trace.
|
|
85
|
+
{
|
|
86
|
+
name: "bearer-header",
|
|
87
|
+
certain: true,
|
|
88
|
+
re: /\bAuthorization:\s*(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,}/gi,
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// --- PII and environment: a shape, so reported rather than certain -------
|
|
92
|
+
{
|
|
93
|
+
name: "email-address",
|
|
94
|
+
certain: false,
|
|
95
|
+
re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
96
|
+
},
|
|
97
|
+
// An absolute path under /Users or /home names the operator.
|
|
98
|
+
{ name: "home-directory-path", certain: false, re: /\/(?:Users|home)\/[A-Za-z0-9_.-]+\//g },
|
|
99
|
+
{
|
|
100
|
+
name: "private-ip",
|
|
101
|
+
certain: false,
|
|
102
|
+
re: /\b(?:10\.\d{1,3}|192\.168|172\.(?:1[6-9]|2\d|3[01]))\.\d{1,3}\.\d{1,3}\b/g,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} text
|
|
108
|
+
* @returns {{rule: string, certain: boolean, line: number}[]}
|
|
109
|
+
*/
|
|
110
|
+
export function scan(text) {
|
|
111
|
+
const findings = [];
|
|
112
|
+
const lines = String(text).split("\n");
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
for (const rule of RULES) {
|
|
115
|
+
// `lastIndex` survives between calls on a /g regex and would make every
|
|
116
|
+
// second line skip its start. Reset per line.
|
|
117
|
+
rule.re.lastIndex = 0;
|
|
118
|
+
if (rule.re.test(lines[i])) {
|
|
119
|
+
findings.push({ rule: rule.name, certain: rule.certain, line: i + 1 });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return findings;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readInput(args) {
|
|
127
|
+
const fileFlag = args.indexOf("--file");
|
|
128
|
+
if (fileFlag !== -1) {
|
|
129
|
+
const p = args[fileFlag + 1];
|
|
130
|
+
if (!p) {
|
|
131
|
+
process.stderr.write("outbound-gate: --file needs a path\n");
|
|
132
|
+
process.exitCode = 2;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return readFileSync(p, "utf-8");
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return readFileSync(0, "utf-8");
|
|
139
|
+
} catch {
|
|
140
|
+
process.stderr.write("outbound-gate: nothing on stdin and no --file\n");
|
|
141
|
+
process.exitCode = 2;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function main() {
|
|
147
|
+
const args = process.argv.slice(2);
|
|
148
|
+
const text = readInput(args);
|
|
149
|
+
if (text === null) return;
|
|
150
|
+
|
|
151
|
+
const findings = scan(text);
|
|
152
|
+
const blocking = findings.filter((f) => f.certain);
|
|
153
|
+
|
|
154
|
+
if (args.includes("--json")) {
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
JSON.stringify({ findings, blocked: blocking.length > 0 }, null, 2) + "\n",
|
|
157
|
+
);
|
|
158
|
+
} else if (findings.length) {
|
|
159
|
+
// Rule name and line only. The value is deliberately absent: writing it
|
|
160
|
+
// here would put the secret into a terminal, a log and a CI transcript.
|
|
161
|
+
const shown = findings.map(
|
|
162
|
+
(f) => ` ${f.certain ? "BLOCK" : "check"} ${f.rule} line ${f.line}`,
|
|
163
|
+
);
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`outbound-gate: ${findings.length} finding(s) in the text about to be published\n` +
|
|
166
|
+
shown.join("\n") +
|
|
167
|
+
"\n (values are not printed - reading them out would move the secret here)\n",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (blocking.length && !args.includes("--warn")) process.exitCode = 7;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isDirectRun = invokedDirectly(import.meta.url);
|
|
175
|
+
if (isDirectRun) main();
|