@mmerterden/multi-agent-pipeline 17.6.0 → 18.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 +127 -0
- package/README.md +43 -1
- package/README.tr.md +41 -0
- package/docs/adr/0011-dormant-ci.md +25 -1
- package/docs/server-readiness.md +188 -0
- package/index.js +16 -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/manifest.json +1049 -0
- package/package.json +5 -2
- package/pipeline/commands/multi-agent/status/SKILL.md +52 -21
- 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/fatal.mjs +129 -0
- 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/outbound-gate.mjs +175 -0
- package/pipeline/lib/plan-todos.sh +27 -6
- 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/run-paths.sh +335 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +70 -0
- package/pipeline/multi-agent-refs/features/cost-analysis.md +93 -0
- package/pipeline/multi-agent-refs/features/doctor.md +45 -0
- package/pipeline/multi-agent-refs/features/verify.md +83 -0
- package/pipeline/multi-agent-refs/phases/operations.md +13 -2
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/unattended-contract.md +129 -0
- package/pipeline/scripts/_run-paths.mjs +372 -0
- package/pipeline/scripts/aggregate-metrics.mjs +64 -64
- 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/classify-plan-safety.mjs +2 -1
- 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 +6 -9
- package/pipeline/scripts/diff-risk-score.mjs +2 -1
- package/pipeline/scripts/doctor.mjs +138 -4
- package/pipeline/scripts/evidence-gate.mjs +9 -3
- package/pipeline/scripts/feedback-send.mjs +12 -2
- package/pipeline/scripts/gc-abandoned.sh +29 -13
- package/pipeline/scripts/gc-worktrees.sh +11 -4
- package/pipeline/scripts/github-ssh-setup.sh +64 -7
- package/pipeline/scripts/graph-mermaid.mjs +4 -2
- package/pipeline/scripts/keychain-save.sh +101 -30
- package/pipeline/scripts/learn-from-transcripts.mjs +2 -1
- package/pipeline/scripts/learning-curve.mjs +34 -29
- package/pipeline/scripts/make-manifest.mjs +199 -0
- package/pipeline/scripts/migrate-prefs.mjs +2 -1
- package/pipeline/scripts/migrate-state.mjs +94 -4
- package/pipeline/scripts/phase-banner.sh +6 -2
- package/pipeline/scripts/phase-tracker.sh +41 -3
- package/pipeline/scripts/plan-coverage-gate.mjs +6 -2
- 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 +9 -2
- package/pipeline/scripts/render-cost-summary.sh +9 -2
- package/pipeline/scripts/render-work-summary.sh +11 -4
- package/pipeline/scripts/review-file-filter.mjs +4 -2
- package/pipeline/scripts/review-scope.mjs +2 -1
- package/pipeline/scripts/routine-registry.mjs +2 -1
- package/pipeline/scripts/run-aggregator.mjs +13 -14
- package/pipeline/scripts/run-metrics.mjs +3 -1
- package/pipeline/scripts/runs-index.mjs +343 -0
- package/pipeline/scripts/scorecard-snapshot.mjs +178 -0
- package/pipeline/scripts/search-logs.sh +18 -0
- package/pipeline/scripts/test-gap-scan.mjs +2 -1
- package/pipeline/scripts/test-integrity-gate.mjs +2 -1
- package/pipeline/scripts/update-issue-progress.sh +56 -7
- package/pipeline/scripts/usage-report.mjs +12 -1
- package/pipeline/scripts/validate-analysis-doc.mjs +2 -1
- 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-test-gap.mjs +6 -3
- package/pipeline/scripts/validate-triage.mjs +3 -1
- package/pipeline/scripts/verify-citations.mjs +4 -2
- package/pipeline/scripts/verify.mjs +327 -0
- package/pipeline/scripts/worktree-finalize.sh +13 -4
- package/pipeline/scripts/write-state.mjs +154 -15
- package/pipeline/skills/.skill-manifest.json +2 -2
- package/pipeline/skills/.skills-index.json +56 -1
- package/pipeline/skills/shared/README.md +8 -3
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +33 -9
- 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 +6 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _unattended-profile.mjs - the permission posture a machine with nobody at
|
|
3
|
+
* the keyboard needs, written only when asked for.
|
|
4
|
+
*
|
|
5
|
+
* autopilot spawns its child with `--permission-prompts none`. That stops Claude
|
|
6
|
+
* Code from ASKING; it grants nothing. The tools the child then calls still have
|
|
7
|
+
* to be allowed somewhere, and on a fresh machine they are not - so the run
|
|
8
|
+
* stops at the first tool call, silently, with no prompt anywhere for a person
|
|
9
|
+
* to answer. It looks exactly like a queue that has nothing to do.
|
|
10
|
+
*
|
|
11
|
+
* This is opt-in and always will be. A default install writes no permissions at
|
|
12
|
+
* all, because widening a permission set is not a thing an installer should do
|
|
13
|
+
* to somebody who only wanted the pipeline files copied. `--unattended` prints
|
|
14
|
+
* the full list and the reason for each line BEFORE writing anything, because
|
|
15
|
+
* "the installer changed my permissions" should never be something a person
|
|
16
|
+
* discovers afterwards.
|
|
17
|
+
*
|
|
18
|
+
* The entries are broad on purpose, and that breadth is the cost of unattended
|
|
19
|
+
* operation rather than an oversight. A pipeline run builds, tests, edits, and
|
|
20
|
+
* shells out to whatever the repo's own toolchain is; an allowlist narrow
|
|
21
|
+
* enough to be interesting would be one the first unfamiliar repo breaks. The
|
|
22
|
+
* honest framing is: this is a machine you have decided to let run on its own.
|
|
23
|
+
*
|
|
24
|
+
* @module install/_unattended-profile
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tool name plus why an unattended run cannot proceed without it. The reason is
|
|
32
|
+
* printed, not just stored: a list of fifteen tool names with no reasons is a
|
|
33
|
+
* list nobody audits.
|
|
34
|
+
*/
|
|
35
|
+
export const UNATTENDED_ALLOW = [
|
|
36
|
+
["Bash", "every build, test, git and gh call the pipeline makes"],
|
|
37
|
+
["Edit", "Phase 3 cannot change a file without it"],
|
|
38
|
+
["Write", "new files, and every artefact a phase leaves behind"],
|
|
39
|
+
["Read", "reading the repo it was pointed at"],
|
|
40
|
+
["Glob", "finding files by pattern instead of shelling out to find"],
|
|
41
|
+
["Grep", "the same, for content"],
|
|
42
|
+
["Agent", "Phase 1 and Phase 4 dispatch subagents"],
|
|
43
|
+
["Skill", "the phase specs are skills"],
|
|
44
|
+
["WebFetch", "issue and ticket bodies referenced by URL"],
|
|
45
|
+
["TaskCreate", "the phase tracker's task list"],
|
|
46
|
+
["TaskUpdate", "the same"],
|
|
47
|
+
["TaskList", "the same"],
|
|
48
|
+
["TaskGet", "the same"],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Render the profile for a human to read before it is applied.
|
|
53
|
+
*
|
|
54
|
+
* @param {string[]} already entries the settings file already carries
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function describeUnattendedProfile(already = []) {
|
|
58
|
+
const lines = [
|
|
59
|
+
" Unattended permission profile - these go into ~/.claude/settings.json",
|
|
60
|
+
" under permissions.allow. Nothing is removed; existing entries are kept.",
|
|
61
|
+
"",
|
|
62
|
+
];
|
|
63
|
+
for (const [tool, why] of UNATTENDED_ALLOW) {
|
|
64
|
+
const mark = already.includes(tool) ? "already there" : "will be added";
|
|
65
|
+
lines.push(` ${tool.padEnd(12)} ${why} (${mark})`);
|
|
66
|
+
}
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(" This is broad, and that breadth is what unattended operation costs: a run");
|
|
69
|
+
lines.push(" builds and tests whatever the target repo uses, so a narrow list is one the");
|
|
70
|
+
lines.push(" first unfamiliar repo stops at, with nobody there to answer.");
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Merge the profile into settings.json. Additive only, and idempotent: running
|
|
76
|
+
* it twice changes nothing the second time, and an entry a person added by hand
|
|
77
|
+
* survives.
|
|
78
|
+
*
|
|
79
|
+
* @param {{home:string, dryRun?:boolean}} opts
|
|
80
|
+
* @returns {{path:string, added:string[], already:string[], written:boolean}}
|
|
81
|
+
*/
|
|
82
|
+
export function applyUnattendedProfile({ home, dryRun = false }) {
|
|
83
|
+
const path = join(home, ".claude", "settings.json");
|
|
84
|
+
let settings = {};
|
|
85
|
+
if (existsSync(path)) {
|
|
86
|
+
try {
|
|
87
|
+
settings = JSON.parse(readFileSync(path, "utf-8"));
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// A settings file that does not parse is the user's, not ours to
|
|
90
|
+
// rewrite: overwriting it would lose whatever is in there.
|
|
91
|
+
throw new Error(`${path} is not valid JSON, so it will not be modified: ${err.message}`, {
|
|
92
|
+
cause: err,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const permissions = settings.permissions || {};
|
|
97
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
98
|
+
const already = [];
|
|
99
|
+
const added = [];
|
|
100
|
+
for (const [tool] of UNATTENDED_ALLOW) {
|
|
101
|
+
if (allow.some((a) => String(a) === tool || String(a).startsWith(`${tool}(`)))
|
|
102
|
+
already.push(tool);
|
|
103
|
+
else {
|
|
104
|
+
allow.push(tool);
|
|
105
|
+
added.push(tool);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (dryRun || !added.length) return { path, added, already, written: false };
|
|
109
|
+
|
|
110
|
+
settings.permissions = { ...permissions, allow };
|
|
111
|
+
writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`);
|
|
112
|
+
return { path, added, already, written: true };
|
|
113
|
+
}
|
package/install/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { installCopilot } from "./copilot.mjs";
|
|
|
20
20
|
import { installCodex } from "./codex.mjs";
|
|
21
21
|
import { setDryRun, writeFile } from "./_common.mjs";
|
|
22
22
|
import { sendInstallTelemetry } from "./_telemetry.mjs";
|
|
23
|
+
import { applyUnattendedProfile, describeUnattendedProfile } from "./_unattended-profile.mjs";
|
|
23
24
|
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
/** Repository root (one level above install/). */
|
|
@@ -63,6 +64,7 @@ export async function runInstall(argv) {
|
|
|
63
64
|
"--index-only",
|
|
64
65
|
"--dry-run",
|
|
65
66
|
"--prune-external",
|
|
67
|
+
"--unattended",
|
|
66
68
|
];
|
|
67
69
|
const KNOWN_PREFIXES = ["--platform="];
|
|
68
70
|
const unknown = flags.filter(
|
|
@@ -91,6 +93,7 @@ export async function runInstall(argv) {
|
|
|
91
93
|
const useSymlinks = flags.includes("--link");
|
|
92
94
|
const indexOnly = flags.includes("--index-only");
|
|
93
95
|
const pruneExternal = flags.includes("--prune-external");
|
|
96
|
+
const unattended = flags.includes("--unattended");
|
|
94
97
|
const platformFlag = parsePlatformFlag(flags);
|
|
95
98
|
|
|
96
99
|
const installerCtx = {
|
|
@@ -117,12 +120,15 @@ export async function runInstall(argv) {
|
|
|
117
120
|
writeVersionMarkers({ home: HOME, forClaude, forCopilot, forCodex });
|
|
118
121
|
|
|
119
122
|
if (dryRun) {
|
|
123
|
+
if (unattended) applyUnattended({ home: HOME, forClaude, dryRun: true });
|
|
120
124
|
console.log("");
|
|
121
125
|
console.log(" Dry-run complete - nothing was written. Re-run without --dry-run to install.");
|
|
122
126
|
console.log("");
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
if (unattended) applyUnattended({ home: HOME, forClaude });
|
|
131
|
+
|
|
126
132
|
printSummary({ forClaude, forCopilot, forCodex });
|
|
127
133
|
|
|
128
134
|
// Fire-and-forget; install is already done.
|
|
@@ -135,6 +141,48 @@ export async function runInstall(argv) {
|
|
|
135
141
|
*
|
|
136
142
|
* @param {{home: string, forClaude: boolean, forCopilot: boolean, forCodex?: boolean}} opts
|
|
137
143
|
*/
|
|
144
|
+
/**
|
|
145
|
+
* Print the unattended permission profile, then apply it. Printing first is the
|
|
146
|
+
* contract: an installer that widens a permission set has to say what it is
|
|
147
|
+
* about to allow while the person can still stop it, not report it afterwards.
|
|
148
|
+
*
|
|
149
|
+
* Claude Code only. The profile is a ~/.claude/settings.json shape, and writing
|
|
150
|
+
* it for a host that does not read that file would be a line in a report that
|
|
151
|
+
* nothing acts on.
|
|
152
|
+
*
|
|
153
|
+
* @param {{home:string, forClaude:boolean, dryRun?:boolean}} opts
|
|
154
|
+
*/
|
|
155
|
+
function applyUnattended({ home, forClaude, dryRun = false }) {
|
|
156
|
+
if (!forClaude) {
|
|
157
|
+
console.log("");
|
|
158
|
+
console.log(" --unattended applies to Claude Code only; nothing written.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let existing = [];
|
|
162
|
+
try {
|
|
163
|
+
const settings = JSON.parse(readFileSync(join(home, ".claude", "settings.json"), "utf-8"));
|
|
164
|
+
existing = (settings?.permissions?.allow ?? []).map(String);
|
|
165
|
+
} catch {
|
|
166
|
+
// No settings file yet, or one that does not parse. Either way there is
|
|
167
|
+
// nothing to mark as "already there"; applyUnattendedProfile decides what
|
|
168
|
+
// to do about the unparseable case.
|
|
169
|
+
}
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log(describeUnattendedProfile(existing));
|
|
172
|
+
try {
|
|
173
|
+
const res = applyUnattendedProfile({ home, dryRun });
|
|
174
|
+
console.log("");
|
|
175
|
+
if (dryRun) console.log(` (dry-run) would add ${res.added.length} entr(ies) to ${res.path}`);
|
|
176
|
+
else if (!res.added.length) console.log(` nothing to add - ${res.path} already covers it`);
|
|
177
|
+
else console.log(` added ${res.added.join(", ")} to ${res.path}`);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
// A settings file we cannot parse is the user's, and rewriting it would
|
|
180
|
+
// lose whatever is in there. Refusing loudly beats a silent overwrite.
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log(` permission profile NOT written: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
138
186
|
export function writeVersionMarkers({ home, forClaude, forCopilot, forCodex }) {
|
|
139
187
|
let version;
|
|
140
188
|
try {
|