devflow-kit 3.10.0 → 3.12.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 +74 -0
- package/dist/agents/architect.md +87 -0
- package/dist/agents/code.md +21 -25
- package/dist/agents/debug.md +21 -49
- package/dist/agents/design.md +9 -43
- package/dist/agents/fix.md +15 -29
- package/dist/agents/git.md +19 -22
- package/dist/agents/harden.md +14 -19
- package/dist/agents/knowledge.md +11 -19
- package/dist/agents/refactor.md +9 -14
- package/dist/agents/research.md +11 -34
- package/dist/agents/review.md +22 -52
- package/dist/agents/skim.md +7 -17
- package/dist/agents/triage.md +7 -14
- package/dist/cli/commands/backup-session.js +131 -0
- package/dist/cli/commands/compliance.js +16 -3
- package/dist/cli/commands/curation.js +12 -4
- package/dist/cli/commands/init.js +36 -55
- package/dist/cli/commands/install-report.js +95 -0
- package/dist/cli/commands/legacy-sweep.js +140 -45
- package/dist/cli/commands/rules.js +50 -13
- package/dist/cli/commands/uninstall.js +62 -24
- package/dist/commands/bug-analysis.md +31 -62
- package/dist/commands/code-review.md +38 -87
- package/dist/commands/debug.md +32 -82
- package/dist/commands/dynamic-build.md +155 -211
- package/dist/commands/dynamic-plan.md +71 -93
- package/dist/commands/dynamic-profile.md +7 -155
- package/dist/commands/dynamic-tickets.md +65 -186
- package/dist/commands/explore.md +27 -70
- package/dist/commands/implement.md +98 -186
- package/dist/commands/plan.md +60 -107
- package/dist/commands/release.md +25 -91
- package/dist/commands/research.md +34 -103
- package/dist/commands/resolve.md +64 -128
- package/dist/commands/self-review.md +35 -99
- package/dist/compliance-off/agents/git.md +15 -18
- package/dist/compliance-off/agents/triage.md +7 -14
- package/dist/compliance-off/commands/bug-analysis.md +31 -62
- package/dist/compliance-off/commands/code-review.md +34 -82
- package/dist/compliance-off/commands/dynamic-build.md +149 -205
- package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
- package/dist/compliance-off/commands/implement.md +94 -180
- package/dist/compliance-off/commands/plan.md +60 -107
- package/dist/compliance-off/commands/release.md +25 -90
- package/dist/compliance-off/commands/resolve.md +54 -128
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- package/dist/core/compliance.js +14 -0
- package/dist/core/fs-atomic.js +5 -1
- package/dist/core/orphan-sweep.js +20 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +6 -5
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- package/dist/curation-compliance-off/agents/triage.md +7 -13
- package/dist/curation-compliance-off/commands/bug-analysis.md +21 -41
- package/dist/curation-compliance-off/commands/code-review.md +25 -62
- package/dist/curation-compliance-off/commands/dynamic-build.md +142 -194
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-compliance-off/commands/implement.md +86 -161
- package/dist/curation-compliance-off/commands/plan.md +51 -86
- package/dist/curation-compliance-off/commands/release.md +17 -78
- package/dist/curation-compliance-off/commands/resolve.md +45 -108
- package/dist/curation-off/agents/architect.md +81 -0
- package/dist/curation-off/agents/code.md +20 -23
- package/dist/curation-off/agents/debug.md +19 -47
- package/dist/curation-off/agents/design.md +8 -41
- package/dist/curation-off/agents/fix.md +14 -27
- package/dist/curation-off/agents/harden.md +13 -18
- package/dist/curation-off/agents/knowledge.md +9 -17
- package/dist/curation-off/agents/refactor.md +8 -12
- package/dist/curation-off/agents/research.md +9 -32
- package/dist/curation-off/agents/review.md +11 -40
- package/dist/curation-off/agents/skim.md +7 -17
- package/dist/curation-off/agents/triage.md +7 -13
- package/dist/curation-off/commands/bug-analysis.md +21 -41
- package/dist/curation-off/commands/code-review.md +29 -67
- package/dist/curation-off/commands/debug.md +24 -63
- package/dist/curation-off/commands/dynamic-build.md +148 -200
- package/dist/curation-off/commands/dynamic-plan.md +60 -80
- package/dist/curation-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-off/commands/explore.md +18 -50
- package/dist/curation-off/commands/implement.md +90 -167
- package/dist/curation-off/commands/plan.md +51 -86
- package/dist/curation-off/commands/release.md +17 -79
- package/dist/curation-off/commands/research.md +23 -81
- package/dist/curation-off/commands/resolve.md +55 -108
- package/dist/curation-off/commands/self-review.md +26 -78
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- package/dist/targets/claude-code/compliance-install.js +38 -13
- package/dist/targets/claude-code/compliance-off-files.js +5 -1
- package/dist/targets/claude-code/curation-install.js +77 -32
- package/dist/targets/claude-code/folder-swap.js +142 -0
- package/dist/targets/claude-code/install-backup.js +780 -0
- package/dist/targets/claude-code/installer.js +383 -192
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +5 -11
- package/dist/targets/claude-code/reference-converge.js +15 -2
- package/dist/targets/claude-code/rules-folder.js +34 -0
- package/dist/targets/claude-code/shipped-set.js +160 -0
- package/dist/targets/claude-code/workflow-folder-install.js +32 -12
- package/dist/variant-axes.json +5 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +3 -5
- package/src/assets/agents/_partials/_design.mds +86 -0
- package/src/assets/agents/architect.mds +41 -0
- package/src/assets/agents/code.mds +18 -20
- package/src/assets/agents/curate.md +26 -35
- package/src/assets/agents/debug.mds +21 -49
- package/src/assets/agents/design.mds +11 -114
- package/src/assets/agents/evaluate.md +6 -16
- package/src/assets/agents/explore.md +1 -2
- package/src/assets/agents/fix.mds +12 -24
- package/src/assets/agents/git.mds +20 -23
- package/src/assets/agents/harden.mds +14 -19
- package/src/assets/agents/knowledge.mds +10 -38
- package/src/assets/agents/polish.md +13 -39
- package/src/assets/agents/refactor.mds +6 -9
- package/src/assets/agents/research.mds +11 -34
- package/src/assets/agents/review.mds +12 -84
- package/src/assets/agents/skim.mds +7 -17
- package/src/assets/agents/synthesize.md +3 -21
- package/src/assets/agents/test.md +31 -32
- package/src/assets/agents/tracker.md +116 -229
- package/src/assets/agents/triage.mds +7 -26
- package/src/assets/agents/validate.md +1 -3
- package/src/assets/commands/_partials/_compliance.mds +2 -2
- package/src/assets/commands/_partials/_decisions.mds +17 -24
- package/src/assets/commands/_partials/_docs_root.mds +31 -10
- package/src/assets/commands/_partials/_engine.mds +34 -56
- package/src/assets/commands/_partials/_evidence_policy.mds +1 -1
- package/src/assets/commands/_partials/_factory.mds +10 -104
- package/src/assets/commands/_partials/_knowledge.mds +47 -51
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +21 -21
- package/src/assets/commands/_partials/_publication.mds +3 -3
- package/src/assets/commands/_partials/_roster.mds +12 -13
- package/src/assets/commands/_partials/_settings.mds +6 -3
- package/src/assets/commands/_partials/_ticket_template.mds +3 -7
- package/src/assets/commands/_partials/_tracker.mds +10 -5
- package/src/assets/commands/_partials/_wave.mds +23 -32
- package/src/assets/commands/bug-analysis.mds +3 -7
- package/src/assets/commands/code-review.mds +12 -33
- package/src/assets/commands/debug.mds +5 -47
- package/src/assets/commands/dynamic-build.mds +48 -70
- package/src/assets/commands/dynamic-plan.mds +23 -42
- package/src/assets/commands/dynamic-profile.mds +7 -57
- package/src/assets/commands/dynamic-tickets.mds +9 -29
- package/src/assets/commands/explore.mds +5 -44
- package/src/assets/commands/implement.mds +75 -117
- package/src/assets/commands/plan.mds +27 -57
- package/src/assets/commands/release.mds +4 -62
- package/src/assets/commands/research.mds +7 -53
- package/src/assets/commands/resolve.mds +40 -87
- package/src/assets/commands/self-review.mds +5 -45
- package/src/assets/mds/git/_ops_pr.mds +1 -0
- package/src/assets/mds/git/_ops_steps.mds +4 -4
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/rules/engineering.md +1 -1
- package/src/assets/rules/quality.md +2 -2
- package/src/assets/scripts/ci-wait.cjs +150 -18
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
- package/src/assets/scripts/hooks/preamble +2 -3
- package/src/assets/scripts/pr-evidence.cjs +1 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +14 -13
- package/src/assets/scripts/resolve-root.cjs +186 -0
- package/src/assets/scripts/resolve-settings.cjs +14 -9
- package/src/assets/scripts/verify-evidence.cjs +355 -22
- package/src/assets/skills/accessibility/SKILL.md +28 -95
- package/src/assets/skills/agent-references/SKILL.md +2 -4
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +1 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +4 -7
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +1 -1
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +5 -6
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +3 -13
- package/src/assets/skills/architecture/SKILL.md +29 -105
- package/src/assets/skills/boundary-validation/SKILL.md +27 -78
- package/src/assets/skills/complexity/SKILL.md +8 -104
- package/src/assets/skills/compliance/SKILL.md +9 -67
- package/src/assets/skills/consistency/SKILL.md +7 -107
- package/src/assets/skills/database/SKILL.md +7 -72
- package/src/assets/skills/dependencies/SKILL.md +9 -109
- package/src/assets/skills/dependencies/references/detection.md +10 -0
- package/src/assets/skills/dependency-research/SKILL.md +9 -63
- package/src/assets/skills/design-review/SKILL.md +1 -35
- package/src/assets/skills/docs-framework/SKILL.md +4 -47
- package/src/assets/skills/documentation/SKILL.md +7 -71
- package/src/assets/skills/documentation/references/patterns.md +1 -1
- package/src/assets/skills/documentation/references/violations.md +1 -1
- package/src/assets/skills/feature-knowledge/SKILL.md +19 -222
- package/src/assets/skills/feature-knowledge/references/new-kb.md +128 -0
- package/src/assets/skills/gap-analysis/SKILL.md +1 -31
- package/src/assets/skills/git/SKILL.md +2 -22
- package/src/assets/skills/go/SKILL.md +24 -113
- package/src/assets/skills/java/SKILL.md +12 -98
- package/src/assets/skills/patterns/SKILL.md +8 -93
- package/src/assets/skills/performance/SKILL.md +29 -88
- package/src/assets/skills/python/SKILL.md +22 -120
- package/src/assets/skills/qa/SKILL.md +16 -54
- package/src/assets/skills/qa/references/browser-testing.md +0 -24
- package/src/assets/skills/qa/references/sources.md +2 -2
- package/src/assets/skills/quality-gates/SKILL.md +13 -100
- package/src/assets/skills/react/SKILL.md +33 -95
- package/src/assets/skills/regression/SKILL.md +8 -73
- package/src/assets/skills/reliability/SKILL.md +11 -120
- package/src/assets/skills/research-codebase/SKILL.md +7 -49
- package/src/assets/skills/research-competitor/SKILL.md +4 -39
- package/src/assets/skills/research-external/SKILL.md +5 -43
- package/src/assets/skills/research-market/SKILL.md +5 -43
- package/src/assets/skills/research-technology/SKILL.md +8 -68
- package/src/assets/skills/review-methodology/SKILL.md +10 -56
- package/src/assets/skills/rust/SKILL.md +19 -109
- package/src/assets/skills/security/SKILL.md +39 -66
- package/src/assets/skills/software-design/SKILL.md +20 -105
- package/src/assets/skills/software-design/references/patterns.md +84 -2
- package/src/assets/skills/software-design/references/violations.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +32 -107
- package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +33 -1
- package/src/assets/skills/testing/SKILL.md +35 -53
- package/src/assets/skills/typescript/SKILL.md +23 -102
- package/src/assets/skills/ui-design/SKILL.md +26 -74
- package/src/assets/skills/worktree-support/SKILL.md +4 -10
- package/dist/curation-off/commands/dynamic-profile.md +0 -214
|
@@ -9,24 +9,36 @@
|
|
|
9
9
|
// only (D-CI-WORKFLOWS): at most one answered probe per run, and its answer is kept for
|
|
10
10
|
// the run; a failed probe is a failed poll, and the next poll probes again. It waits for
|
|
11
11
|
// CI to start until CI_WAIT_MAX_SECONDS, classifies the head it was told about, and
|
|
12
|
-
// prints
|
|
12
|
+
// prints one CI line. With --waits and --fixes it prints one more line, the next
|
|
13
|
+
// action of the CI gate (D-CI-NEXT).
|
|
13
14
|
// Installed beside hud.sh and redact-secrets.cjs under ~/.devflow/scripts/.
|
|
14
15
|
//
|
|
15
16
|
// Usage:
|
|
16
|
-
// node ci-wait.cjs --pr <n> --head <sha>
|
|
17
|
+
// node ci-wait.cjs --pr <n> --head <sha> [--waits <w> --fixes <f>]
|
|
17
18
|
// <n> the pull request number: digits, no leading zero, at most 9
|
|
18
19
|
// <sha> the pushed head: 40 lowercase hex
|
|
20
|
+
// <w> the waits run so far, this one included: 1 to 3
|
|
21
|
+
// <f> the Fix agents run so far: 0 to 2
|
|
22
|
+
// Give --waits and --fixes together or not at all.
|
|
19
23
|
//
|
|
20
24
|
// Run it from the worktree, so gh finds the repository from the working directory.
|
|
21
25
|
//
|
|
22
|
-
// stdout (D-CI-LINE) is
|
|
26
|
+
// stdout (D-CI-LINE) is one line, ending in a newline:
|
|
23
27
|
// CI <STATUS> pr=<n> head=<first 7 hex> failing=<names|-> pending=<names|-> waited=<seconds>
|
|
24
28
|
// with ` reason=<code>` appended to INDETERMINATE only. STATUS is one of STATUSES,
|
|
25
|
-
// the check-ci-status Output enum.
|
|
29
|
+
// the check-ci-status Output enum. With --waits and --fixes (D-CI-NEXT) a second line
|
|
30
|
+
// follows the CI line, ending in a newline:
|
|
31
|
+
// NEXT=<wait|fix|proceed> SAY=<message>
|
|
32
|
+
// where the message is one fixed phrase of this file, or `none`. The line is the gate
|
|
33
|
+
// table of /implement and /resolve as a pure function of the status, the reason, the
|
|
34
|
+
// seconds waited and the two counts, so a prompt only acts on the answer. An argv that
|
|
35
|
+
// fails its gate prints the short form
|
|
26
36
|
// `CI INDETERMINATE pr=- head=- waited=0 reason=usage` and makes no gh call. The
|
|
27
|
-
// boundary re-checks the line against LINE_RE and the 400-character bound
|
|
28
|
-
//
|
|
29
|
-
//
|
|
37
|
+
// boundary re-checks the CI line against LINE_RE and the 400-character bound, and the
|
|
38
|
+
// NEXT line against the fixed set NEXT_LINES, before it writes them. A failed check
|
|
39
|
+
// prints the one failure line and no NEXT line, as it does without the two flags.
|
|
40
|
+
// Exit 0 whenever a line was printed; a reader treats a missing or unparseable line, an
|
|
41
|
+
// unlisted status or a non-zero exit as INDETERMINATE.
|
|
30
42
|
//
|
|
31
43
|
// Design constraints (binding). The text of each verdict rule is written once, at the
|
|
32
44
|
// marker named below; this list points to it. The workflow-probe rule has two readers,
|
|
@@ -49,8 +61,11 @@
|
|
|
49
61
|
// way. A silent foreground node call of about 570 s under a Bash timeout of
|
|
50
62
|
// 600000 ms completed in a main-thread `claude -p` probe on 2026-10-09.
|
|
51
63
|
// - D-CI-NO-EXIT: nothing here calls process.exit(). An explicit exit can
|
|
52
|
-
// truncate stdout when it is a pipe, and the
|
|
64
|
+
// truncate stdout when it is a pipe, and the printed lines are the whole result. The
|
|
53
65
|
// boundary sets process.exitCode and lets the event loop drain.
|
|
66
|
+
// - D-CI-NEXT: see nextAction. The gate table is plumbing, not judgment: it maps a
|
|
67
|
+
// classified status and two counts to one action and one fixed phrase. The phrases
|
|
68
|
+
// carry no gh output, so no check name reaches the NEXT line.
|
|
54
69
|
// - D-CI-WAIT-BOUNDS: every loop has a fixed bound (MAX_POLLS, the deadline, the
|
|
55
70
|
// name lists), and every wait is capped at CI_WAIT_MAX_SECONDS.
|
|
56
71
|
//
|
|
@@ -160,6 +175,9 @@ const NAME_TOKEN_RE = /^(?:-|\+[0-9]+)$/;
|
|
|
160
175
|
|
|
161
176
|
const PR_RE = /^[1-9][0-9]{0,8}$/;
|
|
162
177
|
const HEAD_RE = /^[0-9a-f]{40}$/;
|
|
178
|
+
/** D-CI-NEXT: the waits run so far, this one included (1 to MAX_WAITS), and the fixes (0 to MAX_FIXES). */
|
|
179
|
+
const WAITS_RE = /^[1-3]$/;
|
|
180
|
+
const FIXES_RE = /^[0-2]$/;
|
|
163
181
|
const NO_PR_RE = /Could not resolve to a PullRequest/;
|
|
164
182
|
const NO_CHECKS_RE = /no checks reported/i;
|
|
165
183
|
|
|
@@ -173,6 +191,35 @@ const LINE_RE = new RegExp(
|
|
|
173
191
|
|
|
174
192
|
const USAGE_LINE = 'CI INDETERMINATE pr=- head=- waited=0 reason=usage';
|
|
175
193
|
|
|
194
|
+
/**
|
|
195
|
+
* D-CI-NEXT: the budget of one CI gate (3 waits and 2 fixes in all), the actions it
|
|
196
|
+
* answers with and the phrases it says. A phrase is fixed text of this file; none holds
|
|
197
|
+
* gh output. `none` says nothing. After a full-deadline wait that ends in one of
|
|
198
|
+
* FINAL_AFTER_FULL_WAIT, another wait cannot change the answer, so the gate stops.
|
|
199
|
+
* WAITS_RE and FIXES_RE spell the same two bounds.
|
|
200
|
+
*/
|
|
201
|
+
const MAX_WAITS = 3;
|
|
202
|
+
const MAX_FIXES = 2;
|
|
203
|
+
const NEXTS = Object.freeze(['wait', 'fix', 'proceed']);
|
|
204
|
+
const FINAL_AFTER_FULL_WAIT = Object.freeze(['head-mismatch', 'no-workflow-check']);
|
|
205
|
+
const SAY_NONE = 'none';
|
|
206
|
+
const SAY_NO_CI = 'No PR/CI configured, skipping CI validation.';
|
|
207
|
+
const SAY_RUNNING = 'CI still running — verify manually before merging';
|
|
208
|
+
const SAY_UNKNOWN = 'CI status unknown — verify manually before merging';
|
|
209
|
+
const SAY_FIX = 'CI failed — a fix runs for the failing checks';
|
|
210
|
+
const SAY_STILL_FAILS = 'CI still fails after ' + MAX_FIXES + ' fixes — verify manually before merging';
|
|
211
|
+
|
|
212
|
+
/** The only NEXT lines this script prints: an action and a phrase that go together. */
|
|
213
|
+
const NEXT_LINES = Object.freeze([
|
|
214
|
+
'NEXT=wait SAY=' + SAY_NONE,
|
|
215
|
+
'NEXT=fix SAY=' + SAY_FIX,
|
|
216
|
+
'NEXT=proceed SAY=' + SAY_NONE,
|
|
217
|
+
'NEXT=proceed SAY=' + SAY_NO_CI,
|
|
218
|
+
'NEXT=proceed SAY=' + SAY_RUNNING,
|
|
219
|
+
'NEXT=proceed SAY=' + SAY_UNKNOWN,
|
|
220
|
+
'NEXT=proceed SAY=' + SAY_STILL_FAILS,
|
|
221
|
+
]);
|
|
222
|
+
|
|
176
223
|
/**
|
|
177
224
|
* D-CI-WORKFLOWS: the probe, a GET of the repository's workflow list. It is a fixed
|
|
178
225
|
* argv with no field flag, no method flag and no input, and it names neither the PR nor
|
|
@@ -204,6 +251,8 @@ const VIEW_FIELDS = 'headRefOid,mergeable,state';
|
|
|
204
251
|
* @typedef {{ status: string, pr: string, head: string, failing: string[], pending: string[],
|
|
205
252
|
* waited: number, reason: string | null }} CiResult
|
|
206
253
|
* `head` is the first 7 hex characters.
|
|
254
|
+
* @typedef {{ next: string, say: string }} NextAction
|
|
255
|
+
* `next` is one of NEXTS; `say` is one fixed phrase or SAY_NONE (D-CI-NEXT).
|
|
207
256
|
* @typedef {{ pr: string, head: string, exec: ExecFn, clock: Clock, env?: NodeJS.ProcessEnv }} WaitInput
|
|
208
257
|
* @typedef {{ state: 'deadline', rowsRead: boolean } | { state: 'failed' } | { state: 'pending', pending: string[] }
|
|
209
258
|
* | { state: 'open' }
|
|
@@ -231,26 +280,34 @@ const DEADLINE_LAST = Object.freeze({ status: 'INDETERMINATE', reason: 'deadline
|
|
|
231
280
|
// ---------------------------------------------------------------------------
|
|
232
281
|
|
|
233
282
|
/**
|
|
234
|
-
* D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in
|
|
235
|
-
* order, and nothing else.
|
|
236
|
-
*
|
|
283
|
+
* D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in any
|
|
284
|
+
* order, and nothing else. D-CI-NEXT adds an opt-in pair, `--waits <1 to 3>` and
|
|
285
|
+
* `--fixes <0 to 2>`: both or neither, each once, a single digit in its range. Anything
|
|
286
|
+
* that fails is refused before any gh call, so a value never reaches gh's argv unless it
|
|
287
|
+
* is a plain number or a plain SHA. The result holds `waits` and `fixes` only when the
|
|
288
|
+
* pair was given.
|
|
237
289
|
*
|
|
238
290
|
* @param {readonly string[]} args the arguments after the script path
|
|
239
|
-
* @returns {{ pr: string, head: string } | null}
|
|
291
|
+
* @returns {{ pr: string, head: string, waits?: number, fixes?: number } | null}
|
|
240
292
|
*/
|
|
241
293
|
function parseArgs(args) {
|
|
242
|
-
if (!Array.isArray(args) || args.length !== 4) return null;
|
|
294
|
+
if (!Array.isArray(args) || (args.length !== 4 && args.length !== 8)) return null;
|
|
243
295
|
let pr = null;
|
|
244
296
|
let head = null;
|
|
245
|
-
|
|
297
|
+
let waits = null;
|
|
298
|
+
let fixes = null;
|
|
299
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
246
300
|
const flag = args[i];
|
|
247
301
|
const value = args[i + 1];
|
|
248
302
|
if (typeof value !== 'string') return null;
|
|
249
303
|
if (flag === '--pr' && pr === null && PR_RE.test(value)) pr = value;
|
|
250
304
|
else if (flag === '--head' && head === null && HEAD_RE.test(value)) head = value;
|
|
305
|
+
else if (flag === '--waits' && waits === null && WAITS_RE.test(value)) waits = Number(value);
|
|
306
|
+
else if (flag === '--fixes' && fixes === null && FIXES_RE.test(value)) fixes = Number(value);
|
|
251
307
|
else return null;
|
|
252
308
|
}
|
|
253
|
-
|
|
309
|
+
if (pr === null || head === null) return null;
|
|
310
|
+
return waits === null || fixes === null ? { pr, head } : { pr, head, waits, fixes };
|
|
254
311
|
}
|
|
255
312
|
|
|
256
313
|
// ---------------------------------------------------------------------------
|
|
@@ -364,6 +421,63 @@ function checkLine(line) {
|
|
|
364
421
|
return m !== null && (m[1] === 'INDETERMINATE') === (m[4] !== undefined);
|
|
365
422
|
}
|
|
366
423
|
|
|
424
|
+
/**
|
|
425
|
+
* D-CI-NEXT: what the CI gate does with one printed result. The table is the one the
|
|
426
|
+
* /implement and /resolve prompts used to carry:
|
|
427
|
+
* PASSING proceed, saying nothing
|
|
428
|
+
* NO_PR, NO_CI proceed, saying that no CI is configured
|
|
429
|
+
* PENDING wait while fewer than MAX_WAITS waits have run, else proceed
|
|
430
|
+
* INDETERMINATE wait while fewer than MAX_WAITS waits have run, unless the
|
|
431
|
+
* wait ran the whole deadline and the reason is one of
|
|
432
|
+
* FINAL_AFTER_FULL_WAIT; else proceed, saying the status is unknown
|
|
433
|
+
* FAILING fix while fewer than MAX_FIXES fixes have run, else proceed
|
|
434
|
+
* A status outside STATUSES is unknown, which fails closed to the same proceed.
|
|
435
|
+
* `waited` and `reason` are the result's own values, never read back from text.
|
|
436
|
+
*
|
|
437
|
+
* @param {{ status: string, reason: string | null, waited: number }} result
|
|
438
|
+
* @param {number} waits the waits run so far, this one included
|
|
439
|
+
* @param {number} fixes the Fix agents run so far
|
|
440
|
+
* @returns {NextAction}
|
|
441
|
+
*/
|
|
442
|
+
function nextAction(result, waits, fixes) {
|
|
443
|
+
const proceed = say => ({ next: 'proceed', say });
|
|
444
|
+
switch (result.status) {
|
|
445
|
+
case 'PASSING':
|
|
446
|
+
return proceed(SAY_NONE);
|
|
447
|
+
case 'NO_PR':
|
|
448
|
+
case 'NO_CI':
|
|
449
|
+
return proceed(SAY_NO_CI);
|
|
450
|
+
case 'PENDING':
|
|
451
|
+
return waits < MAX_WAITS ? { next: 'wait', say: SAY_NONE } : proceed(SAY_RUNNING);
|
|
452
|
+
case 'INDETERMINATE': {
|
|
453
|
+
const final = result.waited === CI_WAIT_MAX_SECONDS && FINAL_AFTER_FULL_WAIT.includes(result.reason);
|
|
454
|
+
return waits < MAX_WAITS && !final ? { next: 'wait', say: SAY_NONE } : proceed(SAY_UNKNOWN);
|
|
455
|
+
}
|
|
456
|
+
case 'FAILING':
|
|
457
|
+
return fixes < MAX_FIXES ? { next: 'fix', say: SAY_FIX } : proceed(SAY_STILL_FAILS);
|
|
458
|
+
default:
|
|
459
|
+
return proceed(SAY_UNKNOWN);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @param {NextAction} action
|
|
465
|
+
* @returns {string} `NEXT=<action> SAY=<message>`
|
|
466
|
+
*/
|
|
467
|
+
function renderNext(action) {
|
|
468
|
+
return 'NEXT=' + action.next + ' SAY=' + action.say;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Whether a line is one this script may print as the NEXT line: one of NEXT_LINES, whole.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} line
|
|
475
|
+
* @returns {boolean}
|
|
476
|
+
*/
|
|
477
|
+
function checkNext(line) {
|
|
478
|
+
return typeof line === 'string' && NEXT_LINES.includes(line);
|
|
479
|
+
}
|
|
480
|
+
|
|
367
481
|
// ---------------------------------------------------------------------------
|
|
368
482
|
// One bounded gh call
|
|
369
483
|
// ---------------------------------------------------------------------------
|
|
@@ -947,9 +1061,10 @@ async function main(argv, deps) {
|
|
|
947
1061
|
/** @param {string} reason */
|
|
948
1062
|
const indeterminate = reason =>
|
|
949
1063
|
'CI INDETERMINATE pr=' + parsed.pr + ' head=' + parsed.head.slice(0, 7) + ' waited=0 reason=' + reason;
|
|
1064
|
+
let result = null;
|
|
950
1065
|
let line;
|
|
951
1066
|
try {
|
|
952
|
-
|
|
1067
|
+
result = await waitForCi({
|
|
953
1068
|
pr: parsed.pr,
|
|
954
1069
|
head: parsed.head,
|
|
955
1070
|
exec: injected.exec || defaultExec,
|
|
@@ -958,10 +1073,19 @@ async function main(argv, deps) {
|
|
|
958
1073
|
});
|
|
959
1074
|
line = renderLine(result);
|
|
960
1075
|
} catch (_) {
|
|
1076
|
+
result = null;
|
|
961
1077
|
line = indeterminate('gh-failed');
|
|
962
1078
|
}
|
|
963
|
-
if (!checkLine(line))
|
|
964
|
-
|
|
1079
|
+
if (!checkLine(line)) {
|
|
1080
|
+
result = null;
|
|
1081
|
+
line = indeterminate('unparseable');
|
|
1082
|
+
}
|
|
1083
|
+
if (result === null || parsed.waits === undefined || parsed.fixes === undefined) {
|
|
1084
|
+
return { code: 0, stdout: line + '\n' };
|
|
1085
|
+
}
|
|
1086
|
+
const next = renderNext(nextAction(result, parsed.waits, parsed.fixes));
|
|
1087
|
+
if (!checkNext(next)) return { code: 0, stdout: indeterminate('unparseable') + '\n' };
|
|
1088
|
+
return { code: 0, stdout: line + '\n' + next + '\n' };
|
|
965
1089
|
}
|
|
966
1090
|
|
|
967
1091
|
// ---------------------------------------------------------------------------
|
|
@@ -1011,6 +1135,11 @@ module.exports = Object.freeze({
|
|
|
1011
1135
|
NAMES_LISTED,
|
|
1012
1136
|
LINE_RE,
|
|
1013
1137
|
USAGE_LINE,
|
|
1138
|
+
MAX_WAITS,
|
|
1139
|
+
MAX_FIXES,
|
|
1140
|
+
NEXTS,
|
|
1141
|
+
FINAL_AFTER_FULL_WAIT,
|
|
1142
|
+
NEXT_LINES,
|
|
1014
1143
|
WORKFLOWS_ARGS,
|
|
1015
1144
|
parseArgs,
|
|
1016
1145
|
sanitizeName,
|
|
@@ -1024,6 +1153,9 @@ module.exports = Object.freeze({
|
|
|
1024
1153
|
askWorkflows,
|
|
1025
1154
|
renderLine,
|
|
1026
1155
|
checkLine,
|
|
1156
|
+
nextAction,
|
|
1157
|
+
renderNext,
|
|
1158
|
+
checkNext,
|
|
1027
1159
|
waitForCi,
|
|
1028
1160
|
main,
|
|
1029
1161
|
});
|
|
@@ -3,12 +3,12 @@ You are the orchestrator of this session: you coordinate, agents produce.
|
|
|
3
3
|
|
|
4
4
|
Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it, with one bounded exception: the main thread may run a single git, gh or script command whose output stays under about 40 lines, never a diff, log or test run.
|
|
5
5
|
|
|
6
|
-
Routing (Agent tool)
|
|
6
|
+
Routing (Agent tool):
|
|
7
7
|
- Search and listing: Explore. Codebase orientation: Skim.
|
|
8
8
|
- Execution against a spec: Code (write code to a plan; open the prompt with `OPERATION: <mode>`: `issue-fix` for pre-classified review issues, else `implement`), Refactor (a given mechanical change: renames, moves, boilerplate; `OPERATION: edit`), Polish (clarity pass that keeps behaviour), Validate (build, typecheck, lint, test), Git (git/GitHub).
|
|
9
9
|
- Checks: Harden (self-review that fixes, after Code), Evaluate (alignment with request and plan), Test (scenario QA on acceptance criteria; never fixes). Gate failures: Fix (`OPERATION:` `validation-fix`, `alignment-fix`, `qa-fix` or `ci-fix`), else Code (`OPERATION: implement`).
|
|
10
10
|
- Bug fix: Code (`OPERATION: implement`). All debug work (root cause of a reported bug): devflow:debug, else Explore investigates and Code fixes; never the Debug agent.
|
|
11
|
-
- Analysis, design, research: Design, Research, Review, Triage (validate review issues), Synthesize (many or large reports; open its prompt with `Mode: <mode>`).
|
|
11
|
+
- Analysis, design, research: Design, Architect, Research, Review, Triage (validate review issues), Synthesize (many or large reports; open its prompt with `Mode: <mode>`).
|
|
12
12
|
- A new knowledge base, only if the user asks: Knowledge (slug, name, directories).
|
|
13
13
|
- Real-scale work matching a workflow: invoke the full skill instead: devflow:implement, devflow:plan, devflow:research, devflow:explore, devflow:code-review, devflow:resolve, devflow:bug-analysis (hunt bugs in a branch before merge).
|
|
14
14
|
- Anything else: the closest roster agent, never general-purpose.
|
|
@@ -20,5 +20,5 @@ Operating rules:
|
|
|
20
20
|
- Subagents see none of this conversation. Make every delegation self-contained: goal, constraints, relevant session decisions and facts, exact paths. A deliverable that draws on the conversation (issue, PR, report) needs its substance in the prompt, not a pointer.
|
|
21
21
|
- Report cap: ask every direct delegation for a final report of at most about 1,500 tokens: findings, paths and verdicts, not file dumps.
|
|
22
22
|
- Parallelize independent delegations in one message. Git operations stay sequential.
|
|
23
|
-
- Feature knowledge (direct delegations only
|
|
23
|
+
- Feature knowledge (direct delegations only): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass each matching KB's one to three most relevant `## Rules` bullets (verbatim, IDs included; a KB without Rules: Anti-Patterns or Gotchas entries, cited by section), its path and its `##` heading index as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
|
|
24
24
|
- Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with no arguments: the plan is already in this conversation. Do not pause to ask.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# which the installed render drops when implement is absent) and in the dispatch below —
|
|
12
12
|
# update both together if Claude Code changes the handoff format.
|
|
13
13
|
# 2. Slash skip: no reminder for slash commands (/...).
|
|
14
|
-
# 3. Orchestrator reminder (
|
|
14
|
+
# 3. Orchestrator reminder (1 line): all other prompts, and a handoff prompt when implement
|
|
15
15
|
# is not installed, get a generic delegation reminder.
|
|
16
16
|
|
|
17
17
|
# Safe no-op fallback: must exist before hook-bootstrap is sourced.
|
|
@@ -85,8 +85,7 @@ elif [[ "$HEAD" == "/"* ]]; then
|
|
|
85
85
|
dbg "EXIT: slash command — no reminder"
|
|
86
86
|
else
|
|
87
87
|
dbg "ORCHESTRATOR_REMINDER injected"
|
|
88
|
-
json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to
|
|
89
|
-
Keep only judgment work mainline: conversation, decisions, routing, combining a few agent reports."
|
|
88
|
+
json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to a roster agent or a devflow workflow skill; keep only judgment work mainline (conversation, decisions, routing, combining agent reports)."
|
|
90
89
|
fi
|
|
91
90
|
|
|
92
91
|
dbg "=== HOOK COMPLETE ==="
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// `.devflow/policy.json` is detected by presence and never parsed
|
|
16
16
|
// (D-POLICY-JSON-RETIRED).
|
|
17
17
|
//
|
|
18
|
-
// stdout is exactly one line plus "\n"
|
|
18
|
+
// stdout is exactly one line plus "\n" (D-POLICY-LINE):
|
|
19
19
|
// EVIDENCE_POLICY=<required|standard> SOURCE=<file|worktree|default|invalid|error>
|
|
20
20
|
// REF=<branch|none>[ WARN=<w>[,<w>…]] ISSUE_REQUIRED=<bool> APPLY_CONVENTIONS=<bool>
|
|
21
21
|
// REQUIRE_NON_AUTHOR_APPROVAL=<bool>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
//
|
|
26
26
|
// Exit codes (a caller treats EVERY non-zero code as `required`):
|
|
27
27
|
// 0 resolved — the line above
|
|
28
|
-
// 1 usage error —
|
|
28
|
+
// 1 usage error — usage on stderr; prints FAIL_CLOSED_LINE
|
|
29
29
|
// 2 input unusable — <dir> missing or not a directory; prints FAIL_CLOSED_LINE
|
|
30
30
|
// 3 never emitted — this script writes no file, so the write-failure code of
|
|
31
31
|
// redact-secrets.cjs has no arm here and is absent from EXIT_CODES
|
|
@@ -139,8 +139,9 @@ const { SAFE_REF_RE } = projectConfig;
|
|
|
139
139
|
const OUTPUT_LINE_RE = /^EVIDENCE_POLICY=(?<policy>required|standard) SOURCE=(?<source>file|worktree|default|invalid|error) REF=(?<ref>none|(?![A-Za-z0-9._/-]{0,254}\.\.)[A-Za-z0-9][A-Za-z0-9._/-]{0,254})(?: WARN=(?<warn>(?:remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy)(?:,(?:remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy)){0,3}))? ISSUE_REQUIRED=(?<issue>true|false) APPLY_CONVENTIONS=(?<conventions>true|false) REQUIRE_NON_AUTHOR_APPROVAL=(?<approval>true|false)$/;
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
* The line every refusal prints (exits 2, 4 and 5). A constant, so the boundary
|
|
142
|
+
* The line every refusal prints (exits 1, 2, 4 and 5). A constant, so the boundary
|
|
143
143
|
* can never fail to compose it, and `required` in every field a consumer reads.
|
|
144
|
+
* The literal fallback of the _evidence_policy.mds partial is these bytes.
|
|
144
145
|
*/
|
|
145
146
|
const FAIL_CLOSED_LINE =
|
|
146
147
|
'EVIDENCE_POLICY=required SOURCE=error REF=none ISSUE_REQUIRED=true APPLY_CONVENTIONS=true REQUIRE_NON_AUTHOR_APPROVAL=true';
|
|
@@ -196,7 +197,7 @@ const LS_REMOTE_SYMREF_RE = /^ref: refs\/heads\/([^\t\n]{1,255})\tHEAD$/;
|
|
|
196
197
|
* Injected by tests only, to reach the output gate.
|
|
197
198
|
*
|
|
198
199
|
* @typedef {{ code: number, line: string }} MainOutcome
|
|
199
|
-
* `line` is
|
|
200
|
+
* `line` is FAIL_CLOSED_LINE for codes 1, 2, 4 and 5.
|
|
200
201
|
*/
|
|
201
202
|
|
|
202
203
|
/** @type {ParsedPolicy} */
|
|
@@ -1008,8 +1009,8 @@ function passesOutputGate(line, resolution) {
|
|
|
1008
1009
|
|
|
1009
1010
|
/**
|
|
1010
1011
|
* Settle whatever main() returned into what the boundary may print. The code
|
|
1011
|
-
* must be a known exit code and the line must fit it:
|
|
1012
|
-
*
|
|
1012
|
+
* must be a known exit code and the line must fit it: a coherent line for 0,
|
|
1013
|
+
* FAIL_CLOSED_LINE for the refusals. Anything else is refused.
|
|
1013
1014
|
*
|
|
1014
1015
|
* @param {unknown} outcome
|
|
1015
1016
|
* @returns {MainOutcome}
|
|
@@ -1019,11 +1020,10 @@ function settleOutcome(outcome) {
|
|
|
1019
1020
|
if (o === null || typeof o !== 'object' || typeof o.line !== 'string') {
|
|
1020
1021
|
return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
|
|
1021
1022
|
}
|
|
1022
|
-
if (o.code === EXIT_CODES.USAGE) return { code: EXIT_CODES.USAGE, line: '' };
|
|
1023
1023
|
if (o.code === EXIT_CODES.RESOLVED) {
|
|
1024
1024
|
return isCoherentLine(o.line) ? { code: o.code, line: o.line } : { code: EXIT_CODES.OUTPUT_GATE_REFUSED, line: FAIL_CLOSED_LINE };
|
|
1025
1025
|
}
|
|
1026
|
-
if (o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
|
|
1026
|
+
if (o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
|
|
1027
1027
|
return { code: o.code, line: FAIL_CLOSED_LINE };
|
|
1028
1028
|
}
|
|
1029
1029
|
return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
|
|
@@ -1069,7 +1069,7 @@ function main(argv, deps) {
|
|
|
1069
1069
|
const args = parseArgs(argv);
|
|
1070
1070
|
if (args.kind === 'usage') {
|
|
1071
1071
|
process.stderr.write(args.usage + '\n');
|
|
1072
|
-
return { code: EXIT_CODES.USAGE, line:
|
|
1072
|
+
return { code: EXIT_CODES.USAGE, line: FAIL_CLOSED_LINE };
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
1075
1075
|
if (!isUsableDirectory(args.dir)) {
|
|
@@ -1106,9 +1106,9 @@ function main(argv, deps) {
|
|
|
1106
1106
|
//
|
|
1107
1107
|
// This is the ONLY place that writes to stdout and sets process.exitCode.
|
|
1108
1108
|
// It re-checks what main() returned (settleOutcome) before writing, so even a
|
|
1109
|
-
// main() that returned something malformed prints
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1109
|
+
// main() that returned something malformed prints a line that passes the gate
|
|
1110
|
+
// or FAIL_CLOSED_LINE. Its catch is the fail-closed arm for anything main() did
|
|
1111
|
+
// not anticipate.
|
|
1112
1112
|
// ---------------------------------------------------------------------------
|
|
1113
1113
|
|
|
1114
1114
|
if (require.main === module) {
|
|
@@ -1120,7 +1120,7 @@ if (require.main === module) {
|
|
|
1120
1120
|
outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
|
|
1121
1121
|
}
|
|
1122
1122
|
const settled = settleOutcome(outcome);
|
|
1123
|
-
|
|
1123
|
+
process.stdout.write(settled.line + '\n');
|
|
1124
1124
|
process.exitCode = settled.code;
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
@@ -1141,5 +1141,6 @@ module.exports = Object.freeze({
|
|
|
1141
1141
|
complianceDefault,
|
|
1142
1142
|
resolve,
|
|
1143
1143
|
formatLine,
|
|
1144
|
+
settleOutcome,
|
|
1144
1145
|
main,
|
|
1145
1146
|
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/assets/scripts/resolve-root.cjs
|
|
3
|
+
//
|
|
4
|
+
// Prints the two roots a prompt works from, so a command runs ONE call for them:
|
|
5
|
+
// the checkout (where docs artifacts and feature knowledge live) and the ledger
|
|
6
|
+
// (where the curation index lives). Installed as a top-level sibling of
|
|
7
|
+
// resolve-settings.cjs under ~/.devflow/scripts/, and it requires that file: the git
|
|
8
|
+
// call, its default exec and the ledger rule are resolve-settings.cjs's own
|
|
9
|
+
// (gitToplevel, defaultExec, ledgerRootOf). The ledger rule is the one the hooks apply
|
|
10
|
+
// (hooks/resolve-project-root, and its TypeScript twin src/core/ledger-root.ts).
|
|
11
|
+
// The script holds no other judgment.
|
|
12
|
+
//
|
|
13
|
+
// Usage: node resolve-root.cjs [<dir>] (<dir> defaults to cwd)
|
|
14
|
+
//
|
|
15
|
+
// stdout on success is exactly two lines, each ended by "\n":
|
|
16
|
+
// WORKTREE=<absolute path>
|
|
17
|
+
// LEDGER=<absolute path>
|
|
18
|
+
// WORKTREE is git's toplevel for <dir>. LEDGER is the main worktree when <dir> is in
|
|
19
|
+
// a linked worktree and the main worktree holds a `.devflow/` folder that is not the
|
|
20
|
+
// home directory; otherwise it is the toplevel. When git does not know the ledger
|
|
21
|
+
// root, LEDGER is the toplevel. Outside a git repository both lines name the absolute
|
|
22
|
+
// form of <dir>.
|
|
23
|
+
//
|
|
24
|
+
// Fail closed: on any failure stdout stays empty, stderr holds one reason line and the
|
|
25
|
+
// exit code is not 0. The caller then uses its start directory for both roots.
|
|
26
|
+
//
|
|
27
|
+
// Exit codes:
|
|
28
|
+
// 0 resolved — the two lines above
|
|
29
|
+
// 1 usage error — usage on stderr
|
|
30
|
+
// 2 input unusable — <dir> is missing or not a directory, or a path holds a control
|
|
31
|
+
// character (any C0 character, such as CR, LF or NUL)
|
|
32
|
+
// 4 internal error — or git did not answer
|
|
33
|
+
// (the numbers match resolve-settings.cjs; 3 and 5 are never emitted)
|
|
34
|
+
//
|
|
35
|
+
// Design constraints (shared with resolve-settings.cjs):
|
|
36
|
+
// - main() returns {code, line} and never calls process.exit or throws; the single
|
|
37
|
+
// `require.main === module` boundary is the only stdout write and the only
|
|
38
|
+
// exitCode assignment
|
|
39
|
+
// - every subprocess is spawned with an argv array (never a shell), stdin ignored,
|
|
40
|
+
// a timeout and a maxBuffer — resolve-settings.cjs's runGit does this
|
|
41
|
+
// - a local read only: no network, nothing written
|
|
42
|
+
|
|
43
|
+
'use strict';
|
|
44
|
+
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const path = require('path');
|
|
47
|
+
const settings = require('./resolve-settings.cjs');
|
|
48
|
+
|
|
49
|
+
const EXIT_CODES = Object.freeze({
|
|
50
|
+
RESOLVED: 0,
|
|
51
|
+
USAGE: 1,
|
|
52
|
+
INPUT_UNUSABLE: 2,
|
|
53
|
+
INTERNAL_ERROR: 4,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const USAGE = 'Usage: node resolve-root.cjs [<dir>]';
|
|
57
|
+
|
|
58
|
+
/** Any C0 control character: a path that holds one cannot be a one-line answer. */
|
|
59
|
+
const CONTROL_RE = /[\x00-\x1f]/;
|
|
60
|
+
|
|
61
|
+
/** The grammar of a successful answer: two absolute paths, one per line, no control character. */
|
|
62
|
+
const OUTPUT_RE = /^WORKTREE=\/[^\x00-\x1f]*\nLEDGER=\/[^\x00-\x1f]*$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {readonly string[]} argv process.argv
|
|
66
|
+
* @returns {{ kind: 'resolve', dir: string } | { kind: 'usage', usage: string }}
|
|
67
|
+
*/
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const rest = Array.isArray(argv) ? argv.slice(2) : [];
|
|
70
|
+
/** @type {string[]} */
|
|
71
|
+
const positionals = [];
|
|
72
|
+
for (const arg of rest) {
|
|
73
|
+
if (typeof arg !== 'string' || arg.startsWith('-')) {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'usage',
|
|
76
|
+
usage: 'resolve-root: unrecognised argument ' + JSON.stringify(String(arg)).slice(0, 80) + '\n' + USAGE,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
positionals.push(arg);
|
|
80
|
+
}
|
|
81
|
+
if (positionals.length > 1) return { kind: 'usage', usage: USAGE };
|
|
82
|
+
return { kind: 'resolve', dir: positionals.length === 1 ? positionals[0] : process.cwd() };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} dir
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isUsableDirectory(dir) {
|
|
90
|
+
try {
|
|
91
|
+
return fs.statSync(dir).isDirectory();
|
|
92
|
+
} catch (_) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Print one reason line and return the failure outcome.
|
|
99
|
+
*
|
|
100
|
+
* @param {number} code
|
|
101
|
+
* @param {string} reason
|
|
102
|
+
* @returns {{ code: number, line: string }}
|
|
103
|
+
*/
|
|
104
|
+
function fail(code, reason) {
|
|
105
|
+
process.stderr.write('resolve-root: ' + reason + ' — failing closed\n');
|
|
106
|
+
return { code, line: '' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {readonly string[]} argv process.argv
|
|
111
|
+
* @param {{ exec?: (file: string, args: string[], opts: object) => unknown }} [deps] tests only
|
|
112
|
+
* @returns {{ code: number, line: string }}
|
|
113
|
+
*/
|
|
114
|
+
function main(argv, deps) {
|
|
115
|
+
try {
|
|
116
|
+
const exec = deps && typeof deps.exec === 'function' ? deps.exec : settings.defaultExec;
|
|
117
|
+
const args = parseArgs(argv);
|
|
118
|
+
if (args.kind === 'usage') {
|
|
119
|
+
process.stderr.write(args.usage + '\n');
|
|
120
|
+
return { code: EXIT_CODES.USAGE, line: '' };
|
|
121
|
+
}
|
|
122
|
+
const dir = path.resolve(args.dir);
|
|
123
|
+
if (CONTROL_RE.test(dir)) return fail(EXIT_CODES.INPUT_UNUSABLE, 'the directory path holds a control character');
|
|
124
|
+
if (!isUsableDirectory(dir)) {
|
|
125
|
+
return fail(EXIT_CODES.INPUT_UNUSABLE, 'not a usable directory: ' + JSON.stringify(dir).slice(0, 300));
|
|
126
|
+
}
|
|
127
|
+
const toplevel = settings.gitToplevel(exec, dir);
|
|
128
|
+
if (toplevel.kind === 'unknown') return fail(EXIT_CODES.INTERNAL_ERROR, 'git did not answer');
|
|
129
|
+
let worktree = dir;
|
|
130
|
+
let ledger = dir;
|
|
131
|
+
if (toplevel.kind === 'root') {
|
|
132
|
+
worktree = toplevel.root;
|
|
133
|
+
const found = settings.ledgerRootOf(toplevel.root, { exec });
|
|
134
|
+
ledger = found.kind === 'root' ? found.root : toplevel.root;
|
|
135
|
+
}
|
|
136
|
+
if (CONTROL_RE.test(worktree) || CONTROL_RE.test(ledger)) {
|
|
137
|
+
return fail(EXIT_CODES.INPUT_UNUSABLE, 'a root path holds a control character');
|
|
138
|
+
}
|
|
139
|
+
return { code: EXIT_CODES.RESOLVED, line: 'WORKTREE=' + worktree + '\nLEDGER=' + ledger };
|
|
140
|
+
} catch (_) {
|
|
141
|
+
return fail(EXIT_CODES.INTERNAL_ERROR, 'internal error');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Settle whatever main() returned into what the boundary may print: a success carries
|
|
147
|
+
* a line that passes the grammar, and every other outcome prints nothing.
|
|
148
|
+
*
|
|
149
|
+
* @param {unknown} outcome
|
|
150
|
+
* @returns {{ code: number, line: string }}
|
|
151
|
+
*/
|
|
152
|
+
function settleOutcome(outcome) {
|
|
153
|
+
const o = /** @type {any} */ (outcome);
|
|
154
|
+
if (o === null || typeof o !== 'object' || typeof o.line !== 'string' || typeof o.code !== 'number') {
|
|
155
|
+
return { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
156
|
+
}
|
|
157
|
+
if (o.code === EXIT_CODES.RESOLVED) {
|
|
158
|
+
return OUTPUT_RE.test(o.line) ? { code: o.code, line: o.line } : { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
159
|
+
}
|
|
160
|
+
return { code: o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE ? o.code : EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Top-level boundary — the ONLY stdout write and the ONLY exitCode assignment
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
if (require.main === module) {
|
|
168
|
+
let outcome;
|
|
169
|
+
try {
|
|
170
|
+
outcome = main(process.argv);
|
|
171
|
+
} catch (_) {
|
|
172
|
+
process.stderr.write('resolve-root: internal error — failing closed\n');
|
|
173
|
+
outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
174
|
+
}
|
|
175
|
+
const settled = settleOutcome(outcome);
|
|
176
|
+
if (settled.line !== '') process.stdout.write(settled.line + '\n');
|
|
177
|
+
process.exitCode = settled.code;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = Object.freeze({
|
|
181
|
+
EXIT_CODES,
|
|
182
|
+
OUTPUT_RE,
|
|
183
|
+
parseArgs,
|
|
184
|
+
settleOutcome,
|
|
185
|
+
main,
|
|
186
|
+
});
|