@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
|
@@ -369,8 +369,9 @@ function main() {
|
|
|
369
369
|
files: TOP ? rows.slice(0, Number(TOP)) : rows,
|
|
370
370
|
};
|
|
371
371
|
|
|
372
|
+
// Returning, not exiting: this payload carries a row per changed file and
|
|
373
|
+
// process.exit() would cut it at the pipe buffer.
|
|
372
374
|
process.stdout.write(PRETTY ? JSON.stringify(out, null, 2) + "\n" : JSON.stringify(out) + "\n");
|
|
373
|
-
process.exit(0);
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
try {
|
|
@@ -53,6 +53,7 @@ import { join, dirname } from "node:path";
|
|
|
53
53
|
import { homedir } from "node:os";
|
|
54
54
|
import { execFileSync } from "node:child_process";
|
|
55
55
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
56
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
56
57
|
|
|
57
58
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
58
59
|
const HOME = homedir();
|
|
@@ -63,6 +64,12 @@ const JSON_OUT = argv.includes("--json");
|
|
|
63
64
|
const PROBE = argv.includes("--probe");
|
|
64
65
|
const EXPLAIN = argv.includes("--explain");
|
|
65
66
|
const LIST = argv.includes("--list-checks");
|
|
67
|
+
// `--profile server` adds checks that only matter when nobody is at the
|
|
68
|
+
// keyboard. They are ADDITIVE: the default run is byte-for-byte what it was,
|
|
69
|
+
// because a laptop being told it fails a server check would teach people to
|
|
70
|
+
// ignore doctor.
|
|
71
|
+
const profileFlag = argv.find((a) => a.startsWith("--profile="));
|
|
72
|
+
const PROFILE = profileFlag ? profileFlag.slice("--profile=".length) : "";
|
|
66
73
|
const taskFlag = argv.find((a) => a.startsWith("--task-tools="));
|
|
67
74
|
const TASK_TOOLS = taskFlag ? taskFlag.split("=")[1] : null;
|
|
68
75
|
|
|
@@ -89,6 +96,15 @@ const CHECK_IDS = [
|
|
|
89
96
|
"worktree-residue",
|
|
90
97
|
];
|
|
91
98
|
|
|
99
|
+
// Only under `--profile server`. Kept in their own list so `--list-checks`
|
|
100
|
+
// can say which ones a default run does not include.
|
|
101
|
+
const SERVER_CHECK_IDS = [
|
|
102
|
+
"unattended-contract",
|
|
103
|
+
"unattended-permissions",
|
|
104
|
+
"scheduler",
|
|
105
|
+
"keychain-unlock",
|
|
106
|
+
];
|
|
107
|
+
|
|
92
108
|
// Only these six may return BLOCK. Enforced below, not merely documented: a
|
|
93
109
|
// check that returns BLOCK without being listed here is downgraded and the
|
|
94
110
|
// downgrade is reported, because an unenforced rule is a comment.
|
|
@@ -782,21 +798,138 @@ function checkWorktreeResidue() {
|
|
|
782
798
|
|
|
783
799
|
/* ------------------------------------------------------------------ main -- */
|
|
784
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Checks that only matter when nobody is at the keyboard.
|
|
803
|
+
*
|
|
804
|
+
* Every one of them is a way a server run stops WITHOUT SAYING SO - the
|
|
805
|
+
* failure mode this whole profile exists for. A laptop hits none of them
|
|
806
|
+
* because a human is there to answer, unlock and restart, which is exactly why
|
|
807
|
+
* they are not in the default run.
|
|
808
|
+
*
|
|
809
|
+
* None may BLOCK: `MAY_BLOCK` is a closed set and these are readiness, not
|
|
810
|
+
* correctness. A machine that fails all four still runs the pipeline fine with
|
|
811
|
+
* someone watching it.
|
|
812
|
+
*
|
|
813
|
+
* @param {{root: string}|any} resolved the install layout from checkInstallPresent
|
|
814
|
+
*/
|
|
815
|
+
function checkServerProfile(resolved) {
|
|
816
|
+
const root = resolved?.root || join(homedir(), ".claude");
|
|
817
|
+
|
|
818
|
+
// 1. The contract exists on this machine. Without it nothing tells the
|
|
819
|
+
// operator which entry points honour MULTI_AGENT_UNATTENDED.
|
|
820
|
+
const contract = join(root, "multi-agent-refs", "unattended-contract.md");
|
|
821
|
+
if (existsSync(contract)) {
|
|
822
|
+
ok("unattended-contract");
|
|
823
|
+
} else {
|
|
824
|
+
report(
|
|
825
|
+
"unattended-contract",
|
|
826
|
+
"WARN",
|
|
827
|
+
"unattended-contract.md is not installed, so nothing here documents what an unattended run resolves",
|
|
828
|
+
"run /multi-agent:update to refresh the reference tree",
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 2. The permission posture. autopilot spawns `claude --permission-prompts
|
|
833
|
+
// none`, but the tools it then calls still have to be allowed somewhere,
|
|
834
|
+
// and on a fresh machine they are not - the run stops at the first
|
|
835
|
+
// prompt with nobody to answer it.
|
|
836
|
+
const settings = join(root, "settings.json");
|
|
837
|
+
let allow = [];
|
|
838
|
+
try {
|
|
839
|
+
allow = JSON.parse(readFileSync(settings, "utf-8"))?.permissions?.allow ?? [];
|
|
840
|
+
} catch {
|
|
841
|
+
allow = [];
|
|
842
|
+
}
|
|
843
|
+
const needed = ["Bash", "Edit", "Write"];
|
|
844
|
+
const missing = needed.filter((t) => !allow.some((a) => String(a).startsWith(t)));
|
|
845
|
+
if (!missing.length) {
|
|
846
|
+
ok("unattended-permissions");
|
|
847
|
+
} else {
|
|
848
|
+
report(
|
|
849
|
+
"unattended-permissions",
|
|
850
|
+
"WARN",
|
|
851
|
+
`permissions.allow does not cover ${missing.join(", ")}; an unattended run stops at the first prompt`,
|
|
852
|
+
"run /multi-agent:setup, or add them to permissions.allow in settings.json",
|
|
853
|
+
{ settings, allow },
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 3. Something has to start the work. launchd is the only scheduler this
|
|
858
|
+
// ships a template for; a server with no agent loaded runs nothing and
|
|
859
|
+
// looks idle rather than broken.
|
|
860
|
+
let loaded = false;
|
|
861
|
+
try {
|
|
862
|
+
loaded = execFileSync("launchctl", ["list"], { encoding: "utf-8", timeout: 5000 }).includes(
|
|
863
|
+
"multi-agent",
|
|
864
|
+
);
|
|
865
|
+
} catch {
|
|
866
|
+
// launchctl absent or refusing: treat as "nothing is loaded", which is the
|
|
867
|
+
// conservative read - claiming a scheduler we could not see would be worse.
|
|
868
|
+
}
|
|
869
|
+
if (loaded) {
|
|
870
|
+
ok("scheduler");
|
|
871
|
+
} else {
|
|
872
|
+
report(
|
|
873
|
+
"scheduler",
|
|
874
|
+
"WARN",
|
|
875
|
+
"no multi-agent launchd agent is loaded, so nothing starts a queued run on its own",
|
|
876
|
+
"run /multi-agent:autopilot to install the agent, or schedule the runner yourself",
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// 4. The keychain. Every credential this pipeline reads lives there, and a
|
|
881
|
+
// LaunchDaemon started before login gets a LOCKED keychain: each fetch
|
|
882
|
+
// fails, and the failures surface far away as 401s that blame the token.
|
|
883
|
+
// Reading a real credential here would be a network call and a side
|
|
884
|
+
// effect; the presence of a login-keychain path is the cheap proxy.
|
|
885
|
+
let keychain = "";
|
|
886
|
+
try {
|
|
887
|
+
keychain = execFileSync("security", ["default-keychain"], {
|
|
888
|
+
encoding: "utf-8",
|
|
889
|
+
timeout: 5000,
|
|
890
|
+
}).trim();
|
|
891
|
+
} catch {
|
|
892
|
+
// Same shape: unreachable reads as absent.
|
|
893
|
+
}
|
|
894
|
+
if (keychain) {
|
|
895
|
+
ok("keychain-unlock");
|
|
896
|
+
} else {
|
|
897
|
+
report(
|
|
898
|
+
"keychain-unlock",
|
|
899
|
+
"WARN",
|
|
900
|
+
"no default keychain is reachable from this session; a boot-time daemon sees a locked one and every credential read fails as a 401",
|
|
901
|
+
"run the LaunchAgent at login rather than a LaunchDaemon at boot (docs/server-readiness.md)",
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
785
906
|
function main() {
|
|
786
907
|
if (LIST) {
|
|
908
|
+
// Lists what THIS invocation would run, which is what the name promises
|
|
909
|
+
// and what the registry gate compares against the output of a real run.
|
|
910
|
+
// Listing the server checks unconditionally made `--list-checks` disagree
|
|
911
|
+
// with a default run by four, and the gate that counts printed lines
|
|
912
|
+
// against listed checks caught it immediately.
|
|
787
913
|
for (const id of CHECK_IDS) process.stdout.write(`${id}\n`);
|
|
914
|
+
if (PROFILE === "server") for (const id of SERVER_CHECK_IDS) process.stdout.write(`${id}\n`);
|
|
788
915
|
return;
|
|
789
916
|
}
|
|
790
917
|
const unknown = argv.filter(
|
|
791
|
-
(a) =>
|
|
918
|
+
(a) =>
|
|
919
|
+
a.startsWith("--") && !/^--(probe|json|explain|list-checks|task-tools=|profile=)/.test(a),
|
|
792
920
|
);
|
|
793
921
|
if (unknown.length) {
|
|
794
922
|
process.stderr.write(
|
|
795
|
-
`usage: doctor.mjs [--probe] [--json] [--explain] [--task-tools=yes|no] | --list-checks\n`,
|
|
923
|
+
`usage: doctor.mjs [--probe] [--json] [--explain] [--task-tools=yes|no] [--profile=server] | --list-checks\n`,
|
|
796
924
|
);
|
|
797
925
|
process.exitCode = 3;
|
|
798
926
|
return;
|
|
799
927
|
}
|
|
928
|
+
if (PROFILE && PROFILE !== "server") {
|
|
929
|
+
process.stderr.write(`usage: --profile takes "server" (got "${PROFILE}")\n`);
|
|
930
|
+
process.exitCode = 3;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
800
933
|
if (taskFlag && TASK_TOOLS !== "yes" && TASK_TOOLS !== "no") {
|
|
801
934
|
process.stderr.write("usage: --task-tools takes yes or no\n");
|
|
802
935
|
process.exitCode = 3;
|
|
@@ -837,6 +970,7 @@ function main() {
|
|
|
837
970
|
checkMcpSurface();
|
|
838
971
|
checkDiskSpace();
|
|
839
972
|
checkWorktreeResidue();
|
|
973
|
+
if (PROFILE === "server") checkServerProfile(resolved);
|
|
840
974
|
|
|
841
975
|
const blocked = results.filter((r) => r.severity === "BLOCK");
|
|
842
976
|
const warned = results.filter((r) => r.severity === "WARN");
|
|
@@ -852,7 +986,7 @@ function main() {
|
|
|
852
986
|
// Every check prints, in registry order, including the ones that found
|
|
853
987
|
// nothing and the ones that were not run. A list that shows only problems
|
|
854
988
|
// cannot be told apart from a list that was never produced.
|
|
855
|
-
for (const id of CHECK_IDS) {
|
|
989
|
+
for (const id of [...CHECK_IDS, ...SERVER_CHECK_IDS]) {
|
|
856
990
|
const r = results.find((x) => x.id === id);
|
|
857
991
|
if (!r) continue;
|
|
858
992
|
if (r.severity === "OK") {
|
|
@@ -897,5 +1031,5 @@ function invokedDirectly() {
|
|
|
897
1031
|
}
|
|
898
1032
|
|
|
899
1033
|
if (invokedDirectly()) {
|
|
900
|
-
main
|
|
1034
|
+
runMain("doctor", main);
|
|
901
1035
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// 1 - pass claim is UNVERIFIED (no evidence / empty / shows failure / no success marker) -> caller must NOT record passed
|
|
36
36
|
// 2 - usage error
|
|
37
37
|
|
|
38
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
38
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
39
39
|
|
|
40
40
|
// Failure markers are kept DEFINITIVE (not broad) because failure now overrides
|
|
41
41
|
// success (see the decision logic): a bare "error:" or stray "FAILED" in an
|
|
@@ -103,12 +103,18 @@ function parseArgs(argv) {
|
|
|
103
103
|
return out;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Both of these have to stop the run where they are called - the gate's whole
|
|
107
|
+
// contract is "verdict, then nothing else happens" - so process.exit() stays.
|
|
108
|
+
// What changes is the write: console.log to a pipe is asynchronous and exit
|
|
109
|
+
// discards what has not drained, while writeFileSync to fd 1 returns only once
|
|
110
|
+
// the bytes are gone. The verdict is short today; the verdict is also the only
|
|
111
|
+
// thing a caller reads, so losing half of it is the one failure that matters.
|
|
106
112
|
function fail(msg) {
|
|
107
|
-
|
|
113
|
+
writeFileSync(1, `${JSON.stringify({ ok: false, code: 1, reason: msg })}\n`);
|
|
108
114
|
process.exit(1);
|
|
109
115
|
}
|
|
110
116
|
function ok(msg) {
|
|
111
|
-
|
|
117
|
+
writeFileSync(1, `${JSON.stringify({ ok: true, code: 0, reason: msg })}\n`);
|
|
112
118
|
process.exit(0);
|
|
113
119
|
}
|
|
114
120
|
function usage(msg) {
|
|
@@ -138,8 +138,10 @@ async function main() {
|
|
|
138
138
|
};
|
|
139
139
|
|
|
140
140
|
if (process.argv.includes("--dry-run")) {
|
|
141
|
+
// return, not process.exit(0): the whole point of --dry-run is to read this
|
|
142
|
+
// payload, and exiting truncates it whenever stdout is a pipe.
|
|
141
143
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
142
|
-
|
|
144
|
+
return;
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (!endpointAllowed(endpoint)) {
|
|
@@ -180,4 +182,12 @@ async function main() {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
main()
|
|
185
|
+
// `main()` is async, and calling it bare leaves any rejection it does not
|
|
186
|
+
// handle itself as an unhandled rejection: Node prints its own stack trace and
|
|
187
|
+
// exits 1, so the user of a feedback command sees a crash dump instead of the
|
|
188
|
+
// one line this file writes for every other failure. The catch keeps the
|
|
189
|
+
// failure in this file's own voice.
|
|
190
|
+
main().catch((err) => {
|
|
191
|
+
process.stderr.write(`ERROR: feedback-send failed - ${err?.message ?? err}\n`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
#
|
|
65
65
|
# Exit: 0 on success (including "nothing to do"), 2 on usage error.
|
|
66
66
|
|
|
67
|
-
set -
|
|
67
|
+
set -euo pipefail
|
|
68
68
|
|
|
69
69
|
LOGS="$HOME/.claude/logs/multi-agent"
|
|
70
70
|
REPOS="$HOME"
|
|
@@ -121,20 +121,25 @@ HANDLED=""
|
|
|
121
121
|
# sweep that walks one layout leaves the other copy claiming the run is live.
|
|
122
122
|
while IFS= read -r state; do
|
|
123
123
|
[ -f "$state" ] || continue
|
|
124
|
-
|
|
124
|
+
# Every jq read in this loop is forgiven. A malformed state file is exactly
|
|
125
|
+
# what a sweep for abandoned runs expects to meet, and under `set -e` the
|
|
126
|
+
# first one would end the sweep - leaving every run AFTER it untouched and
|
|
127
|
+
# reporting a clean tree. An empty value already means "unknown", and the
|
|
128
|
+
# rules below refuse to delete on unknown.
|
|
129
|
+
status=$(jq -r '.status // ""' "$state" 2>/dev/null || true)
|
|
125
130
|
# Only runs that CLAIM to be running. A file with no status is unknown, and
|
|
126
131
|
# unknown is not a licence to delete.
|
|
127
132
|
case "$status" in in_progress | paused | awaiting_input) ;; *) continue ;; esac
|
|
128
133
|
|
|
129
|
-
task=$(jq -r '.taskId // .jiraId // ""' "$state" 2>/dev/null)
|
|
134
|
+
task=$(jq -r '.taskId // .jiraId // ""' "$state" 2>/dev/null || true)
|
|
130
135
|
[ -n "$task" ] || continue
|
|
131
|
-
phase=$(jq -r '.currentPhase // "?"' "$state" 2>/dev/null)
|
|
136
|
+
phase=$(jq -r '.currentPhase // "?"' "$state" 2>/dev/null || true)
|
|
132
137
|
# `.pr` is an object in the schema and a bare URL string in three state files
|
|
133
138
|
# on disk. `jq '.pr.url'` on a string errors, and with stderr suppressed that
|
|
134
139
|
# reads as "no PR" - so a run whose PR is already open would be classed as dead
|
|
135
140
|
# and reaped. Read the shape, do not assume it.
|
|
136
|
-
pr=$(jq -r 'if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring' "$state" 2>/dev/null)
|
|
137
|
-
wt=$(jq -r '.worktreePath // ""' "$state" 2>/dev/null)
|
|
141
|
+
pr=$(jq -r 'if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring' "$state" 2>/dev/null || true)
|
|
142
|
+
wt=$(jq -r '.worktreePath // ""' "$state" 2>/dev/null || true)
|
|
138
143
|
|
|
139
144
|
# Rule 1: waiting for you is not residue.
|
|
140
145
|
if [ "$status" = "awaiting_input" ] || [ -n "$pr" ] || [ "$phase" = "6" ] || [ "$phase" = "7" ]; then
|
|
@@ -142,7 +147,8 @@ while IFS= read -r state; do
|
|
|
142
147
|
continue
|
|
143
148
|
fi
|
|
144
149
|
|
|
145
|
-
mtime=$(_mtime "$state")
|
|
150
|
+
mtime=$(_mtime "$state" || true)
|
|
151
|
+
[ -n "$mtime" ] || continue
|
|
146
152
|
age_days=$(((NOW - mtime) / 86400))
|
|
147
153
|
if [ "$phase" = "0" ]; then
|
|
148
154
|
limit="$PHASE0_DAYS"; kind="left at a Phase 0 question"
|
|
@@ -155,7 +161,8 @@ while IFS= read -r state; do
|
|
|
155
161
|
# root recorded in worktreePath is the shape that would delete a checkout.
|
|
156
162
|
safe_wt=""
|
|
157
163
|
if [ -n "$wt" ] && [ -d "$wt" ] && [ ! -L "$wt" ]; then
|
|
158
|
-
|
|
164
|
+
# A worktree path that no longer resolves is the ordinary case here.
|
|
165
|
+
real=$(cd "$wt" 2>/dev/null && pwd -P || true)
|
|
159
166
|
case "$real" in
|
|
160
167
|
*/.worktrees/*) safe_wt="$real" ;;
|
|
161
168
|
*) skipped_unsafe=$((skipped_unsafe + 1)) ;;
|
|
@@ -187,8 +194,11 @@ $safe_wt"
|
|
|
187
194
|
fi
|
|
188
195
|
|
|
189
196
|
run_dir=$(dirname "$state")
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
# Forgiven, both of them: this is the salvage copy, and a run whose artefacts
|
|
198
|
+
# cannot be copied is still a run that has to be marked abandoned. Under
|
|
199
|
+
# `set -e` an unreadable state file here would end the entire sweep.
|
|
200
|
+
mkdir -p "$run_dir/artifacts" 2>/dev/null || true
|
|
201
|
+
cp "$state" "$run_dir/artifacts/agent-state.json" 2>/dev/null || true
|
|
192
202
|
tmp="$state.tmp.$$"
|
|
193
203
|
if jq --arg r "abandoned by gc-abandoned after ${age_days}d idle" \
|
|
194
204
|
'.status = "failed" | .haltReason = $r' "$state" > "$tmp" 2>/dev/null; then
|
|
@@ -209,7 +219,8 @@ $safe_wt"
|
|
|
209
219
|
if [ -n "$safe_wt" ]; then
|
|
210
220
|
repo=${safe_wt%%/.worktrees/*}
|
|
211
221
|
git -C "$repo" worktree remove --force "$safe_wt" >/dev/null 2>&1 || rm -rf "$safe_wt"
|
|
212
|
-
|
|
222
|
+
# Forgiven: one flaky prune must not end the sweep before its summary line.
|
|
223
|
+
git -C "$repo" worktree prune >/dev/null 2>&1 || true
|
|
213
224
|
printf '→ removed: %s (%s, idle %sd)\n' "$task" "$human" "$age_days"
|
|
214
225
|
freed_kb=$((freed_kb + ${size_kb:-0}))
|
|
215
226
|
else
|
|
@@ -233,7 +244,11 @@ if [ "$STATE_ONLY" -eq 0 ] && [ -d "$REPOS" ]; then
|
|
|
233
244
|
# re-walk of the log tree for each of 28 directories.
|
|
234
245
|
index=$(find "$LOGS" -name agent-state.json -type f -maxdepth 4 -not -path '*/artifacts/*' 2>/dev/null |
|
|
235
246
|
while IFS= read -r f; do
|
|
236
|
-
|
|
247
|
+
# One malformed state file must not truncate the index: under `set -e`
|
|
248
|
+
# a jq failure inside this subshell ends the loop, and the entries after
|
|
249
|
+
# it would silently not exist - which reads as "no worktree matches" and
|
|
250
|
+
# protects nothing.
|
|
251
|
+
jq -r '[(.worktreePath // ""), (.taskId // .jiraId // ""), (.status // ""), (if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring), ((.currentPhase // "") | tostring)] | @tsv' "$f" 2>/dev/null || true
|
|
237
252
|
done)
|
|
238
253
|
|
|
239
254
|
for wtroot in "$REPOS"/*/.worktrees; do
|
|
@@ -322,7 +337,8 @@ if [ "$STATE_ONLY" -eq 0 ] && [ -d "$REPOS" ]; then
|
|
|
322
337
|
continue
|
|
323
338
|
fi
|
|
324
339
|
git -C "$repo" worktree remove --force "$real" >/dev/null 2>&1 || rm -rf "$real"
|
|
325
|
-
|
|
340
|
+
# Same reason as the first pass: a failed prune is not a reason to stop.
|
|
341
|
+
git -C "$repo" worktree prune >/dev/null 2>&1 || true
|
|
326
342
|
printf '→ removed worktree: %s (%s, %s, idle %sd)\n' "$id" "$why" "$human" "$age_days"
|
|
327
343
|
freed_kb=$((freed_kb + ${size_kb:-0}))
|
|
328
344
|
removed=$((removed + 1))
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
#
|
|
35
35
|
# Exit: 0 on success (including "nothing to do"), 2 on usage error.
|
|
36
36
|
|
|
37
|
-
set -
|
|
37
|
+
set -euo pipefail
|
|
38
38
|
|
|
39
39
|
REPO="$PWD"
|
|
40
40
|
DELETE=0
|
|
@@ -69,7 +69,9 @@ if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
69
69
|
echo "gc-worktrees: $REPO is not a git work tree - nothing to do"
|
|
70
70
|
exit 0
|
|
71
71
|
fi
|
|
72
|
-
|
|
72
|
+
# A repo path that cannot be entered is a usage error with a message, not a
|
|
73
|
+
# silent exit 1 three lines before the message exists.
|
|
74
|
+
REPO="$(cd "$REPO" && pwd -P)" || { echo "gc-worktrees: cannot enter $REPO" >&2; exit 2; }
|
|
73
75
|
# Anchor on the MAIN worktree (first porcelain entry) so a run started from a
|
|
74
76
|
# subdirectory OR from inside a linked worktree still targets
|
|
75
77
|
# <main-repo>/.worktrees/ rather than a bogus <subdir>/.worktrees/ (which
|
|
@@ -107,7 +109,9 @@ EOF
|
|
|
107
109
|
for d in "$WT_ROOT"/*/; do
|
|
108
110
|
[ -d "$d" ] || continue
|
|
109
111
|
[ -L "${d%/}" ] && continue # never follow a symlinked entry out of the tree
|
|
110
|
-
|
|
112
|
+
# A directory that vanished between the glob and this line is ordinary on a
|
|
113
|
+
# machine where runs are finishing; skipping it is the answer, not aborting.
|
|
114
|
+
dir="$(cd "$d" && pwd -P)" || continue
|
|
111
115
|
# Path guard: only ever consider dirs strictly inside <repo>/.worktrees/.
|
|
112
116
|
case "$dir" in "$WT_ROOT"/*) ;; *) continue ;; esac
|
|
113
117
|
if printf '%s\n' "$registered" | grep -qxF "$dir"; then
|
|
@@ -120,7 +124,10 @@ EOF
|
|
|
120
124
|
done
|
|
121
125
|
fi
|
|
122
126
|
for dir in ${orphans+"${orphans[@]}"}; do
|
|
123
|
-
size
|
|
127
|
+
# Cosmetic: the size goes in a report line and prints as "?" when du cannot
|
|
128
|
+
# read it. Under `set -e` with pipefail an unreadable directory would end the
|
|
129
|
+
# sweep instead.
|
|
130
|
+
size=$(du -sh "$dir" 2>/dev/null | cut -f1 || true)
|
|
124
131
|
if [ "$DELETE" -eq 1 ]; then
|
|
125
132
|
rm -rf "$dir" && { echo "→ removed orphan worktree dir: ${dir#"$REPO"/} (${size:-?})"; changed=$((changed + 1)); }
|
|
126
133
|
else
|
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# GitHub SSH Key Oluşturma ve Kurulum Scripti
|
|
3
|
+
#
|
|
4
|
+
# HEADLESS: her soru bir env değişkeniyle önceden cevaplanabilir ve terminal
|
|
5
|
+
# yoksa script SORMAZ. Üç `read -p` koruma olmadan duruyordu; bir launchd
|
|
6
|
+
# ajanının ya da uzak bir kabuğun altında ilki sonsuza kadar bekler ve dışarı
|
|
7
|
+
# hiçbir şey yazmaz, yani kurulum "asılı kaldı" diye görünür. Bir sunucuda
|
|
8
|
+
# çalışması hedeflenen bir aracın sessizce beklemesi, açıkça reddetmesinden
|
|
9
|
+
# kötüdür.
|
|
10
|
+
#
|
|
11
|
+
# MULTI_AGENT_UNATTENDED=1 terminal olsa bile soru sorulmaz
|
|
12
|
+
# SSH_SETUP_EMAIL anahtarın yorum alanına yazılacak e-posta (zorunlu)
|
|
13
|
+
# SSH_SETUP_OVERWRITE e|h - var olan anahtarın üzerine yazılsın mı (varsayılan h)
|
|
14
|
+
# SSH_SETUP_TEST e|h - sonunda github.com'a bağlantı denensin mi (varsayılan h)
|
|
3
15
|
|
|
4
16
|
set -euo pipefail
|
|
5
17
|
|
|
18
|
+
# Soruyu sor, ama yalnız soracak birisi varsa. Terminal yoksa env'deki cevabı
|
|
19
|
+
# kullan; o da yoksa boş dön ve kararı çağırana bırak.
|
|
20
|
+
ask() {
|
|
21
|
+
local prompt="$1" preset="$2" __var="$3" reply=""
|
|
22
|
+
if [ -n "$preset" ]; then
|
|
23
|
+
printf '%s%s [env]\n' "$prompt" "$preset"
|
|
24
|
+
printf -v "$__var" '%s' "$preset"
|
|
25
|
+
return 0
|
|
26
|
+
fi
|
|
27
|
+
# "Terminal yok" başsızlığın olağan biçimi, tek biçimi değil: screen,
|
|
28
|
+
# tmux ya da bir sunucudaki login kabuğunun terminali VARDIR ve önünde
|
|
29
|
+
# kimse yoktur. MULTI_AGENT_UNATTENDED=1 operatörün bunu açıkça söylemesi;
|
|
30
|
+
# refs/unattended-contract.md. Tanımsızken hiçbir şey değişmez.
|
|
31
|
+
if [ ! -t 0 ] || [ "${MULTI_AGENT_UNATTENDED:-}" = "1" ]; then
|
|
32
|
+
printf -v "$__var" '%s' ""
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
# EOF (Ctrl-D) is an answer, not a crash: without `|| true` `set -e`
|
|
36
|
+
# turns a user pressing Ctrl-D into an exit with no message at all.
|
|
37
|
+
read -r -p "$prompt" reply || true
|
|
38
|
+
printf -v "$__var" '%s' "$reply"
|
|
39
|
+
}
|
|
40
|
+
|
|
6
41
|
echo "=== GitHub SSH Key Kurulumu ==="
|
|
7
42
|
echo ""
|
|
8
43
|
|
|
9
44
|
# E-posta adresi al
|
|
10
|
-
|
|
45
|
+
ask "GitHub e-posta adresinizi girin: " "${SSH_SETUP_EMAIL:-}" EMAIL
|
|
11
46
|
|
|
12
47
|
if [ -z "$EMAIL" ]; then
|
|
13
|
-
|
|
48
|
+
if [ -t 0 ] && [ "${MULTI_AGENT_UNATTENDED:-}" != "1" ]; then
|
|
49
|
+
echo "Hata: E-posta adresi boş olamaz."
|
|
50
|
+
else
|
|
51
|
+
if [ "${MULTI_AGENT_UNATTENDED:-}" = "1" ]; then
|
|
52
|
+
echo "Hata: MULTI_AGENT_UNATTENDED=1 ve SSH_SETUP_EMAIL tanımlı değil." >&2
|
|
53
|
+
else
|
|
54
|
+
echo "Hata: terminal yok ve SSH_SETUP_EMAIL tanımlı değil." >&2
|
|
55
|
+
fi
|
|
56
|
+
echo " Başsız çalıştırmak için: SSH_SETUP_EMAIL=you@example.com $0" >&2
|
|
57
|
+
fi
|
|
14
58
|
exit 1
|
|
15
59
|
fi
|
|
16
60
|
|
|
@@ -26,7 +70,9 @@ chmod 700 "$HOME/.ssh"
|
|
|
26
70
|
if [ -f "$KEY_PATH" ]; then
|
|
27
71
|
echo ""
|
|
28
72
|
echo "Uyari: $KEY_PATH zaten mevcut."
|
|
29
|
-
|
|
73
|
+
# Varsayılan HAYIR. Başsız bir koşuda cevapsız kalan bir "üzerine yazayım
|
|
74
|
+
# mı" sorusunun güvenli tarafı, var olan anahtarı korumaktır.
|
|
75
|
+
ask "Üzerine yazmak istiyor musunuz? (e/h): " "${SSH_SETUP_OVERWRITE:-}" OVERWRITE
|
|
30
76
|
if [ "$OVERWRITE" != "e" ]; then
|
|
31
77
|
echo "İşlem iptal edildi."
|
|
32
78
|
exit 0
|
|
@@ -36,7 +82,11 @@ fi
|
|
|
36
82
|
# SSH key oluştur (Ed25519 - modern ve güvenli)
|
|
37
83
|
echo ""
|
|
38
84
|
echo "SSH key oluşturuluyor..."
|
|
39
|
-
ssh-keygen
|
|
85
|
+
# ssh-keygen var olan bir dosyayı görünce KENDİ "Overwrite (y/n)?" sorusunu
|
|
86
|
+
# sorar - yani yukarıdaki onayı geçtikten sonra başsız koşu ikinci bir
|
|
87
|
+
# soruda asılır. Onay zaten alındı; dosyayı önce kaldır.
|
|
88
|
+
rm -f "$KEY_PATH" "${KEY_PATH}.pub"
|
|
89
|
+
ssh-keygen -t ed25519 -C "$EMAIL" -f "$KEY_PATH" -N "" -q
|
|
40
90
|
|
|
41
91
|
echo ""
|
|
42
92
|
echo "SSH key başarıyla oluşturuldu!"
|
|
@@ -66,7 +116,12 @@ EOF
|
|
|
66
116
|
fi
|
|
67
117
|
|
|
68
118
|
# Key'i agent'a ekle (macOS Keychain ile)
|
|
69
|
-
|
|
119
|
+
# Agent yoksa ekleme başarısız olur; anahtar yazıldıktan sonra bunun için
|
|
120
|
+
# çıkmak, kurulumu yarıda bırakıp kullanıcıya hiçbir sonraki adımı
|
|
121
|
+
# göstermemek demek.
|
|
122
|
+
ssh-add --apple-use-keychain "$KEY_PATH" 2>/dev/null \
|
|
123
|
+
|| ssh-add "$KEY_PATH" 2>/dev/null \
|
|
124
|
+
|| echo "[Uyari: anahtar ssh-agent'a eklenemedi - elle: ssh-add $KEY_PATH]"
|
|
70
125
|
|
|
71
126
|
# Public key'i göster ve kopyala
|
|
72
127
|
echo ""
|
|
@@ -97,11 +152,13 @@ echo "4. 'Add SSH key' butonuna tıklayın"
|
|
|
97
152
|
echo ""
|
|
98
153
|
|
|
99
154
|
# GitHub'a key eklendikten sonra test
|
|
100
|
-
|
|
155
|
+
ask "Key'i GitHub'a ekledikten sonra test etmek ister misiniz? (e/h): " "${SSH_SETUP_TEST:-}" TEST
|
|
101
156
|
if [ "$TEST" = "e" ]; then
|
|
102
157
|
echo ""
|
|
103
158
|
echo "GitHub bağlantısı test ediliyor..."
|
|
104
|
-
|
|
159
|
+
# Bilinmeyen host anahtarı üçüncü bir interaktif soru demek; başsız
|
|
160
|
+
# koşuda orada asılırdı.
|
|
161
|
+
ssh -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 || true
|
|
105
162
|
fi
|
|
106
163
|
|
|
107
164
|
echo ""
|
|
@@ -41,6 +41,8 @@ import { resolve } from "node:path";
|
|
|
41
41
|
import { parseFlags } from "./graph-build.mjs";
|
|
42
42
|
import { loadGraph, defaultGraphPath } from "./graph-query.mjs";
|
|
43
43
|
import { findByName, affected } from "./graph-affected.mjs";
|
|
44
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
45
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
44
46
|
|
|
45
47
|
const DEFAULT_MAX_NODES = 25;
|
|
46
48
|
|
|
@@ -246,6 +248,6 @@ function main() {
|
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
if (import.meta.url
|
|
250
|
-
main
|
|
251
|
+
if (invokedDirectly(import.meta.url)) {
|
|
252
|
+
runMain("graph-mermaid", main);
|
|
251
253
|
}
|