devflow-kit 3.11.0 → 3.12.1
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 +73 -0
- package/dist/agents/architect.md +91 -0
- package/dist/agents/code.md +21 -25
- package/dist/agents/debug.md +22 -50
- 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/uninstall.js +1 -1
- package/dist/commands/bug-analysis.md +32 -63
- package/dist/commands/code-review.md +39 -88
- package/dist/commands/debug.md +32 -82
- package/dist/commands/dynamic-build.md +157 -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 +101 -189
- package/dist/commands/plan.md +61 -108
- package/dist/commands/release.md +26 -92
- package/dist/commands/research.md +37 -106
- package/dist/commands/resolve.md +65 -129
- package/dist/commands/self-review.md +36 -100
- 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 +32 -63
- package/dist/compliance-off/commands/code-review.md +35 -83
- package/dist/compliance-off/commands/dynamic-build.md +151 -205
- package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
- package/dist/compliance-off/commands/implement.md +97 -183
- package/dist/compliance-off/commands/plan.md +61 -108
- package/dist/compliance-off/commands/release.md +26 -91
- package/dist/compliance-off/commands/resolve.md +55 -129
- 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/plugins.js +3 -3
- package/dist/curation-compliance-off/agents/triage.md +7 -13
- package/dist/curation-compliance-off/commands/bug-analysis.md +22 -42
- package/dist/curation-compliance-off/commands/code-review.md +26 -63
- package/dist/curation-compliance-off/commands/dynamic-build.md +144 -194
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-compliance-off/commands/implement.md +89 -164
- package/dist/curation-compliance-off/commands/plan.md +52 -87
- package/dist/curation-compliance-off/commands/release.md +18 -79
- package/dist/curation-compliance-off/commands/resolve.md +46 -109
- package/dist/curation-off/agents/architect.md +85 -0
- package/dist/curation-off/agents/code.md +20 -23
- package/dist/curation-off/agents/debug.md +20 -48
- 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 +22 -42
- package/dist/curation-off/commands/code-review.md +30 -68
- package/dist/curation-off/commands/debug.md +24 -63
- package/dist/curation-off/commands/dynamic-build.md +150 -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 +93 -170
- package/dist/curation-off/commands/plan.md +52 -87
- package/dist/curation-off/commands/release.md +18 -80
- package/dist/curation-off/commands/research.md +26 -84
- package/dist/curation-off/commands/resolve.md +56 -109
- package/dist/curation-off/commands/self-review.md +27 -79
- package/dist/hud/components/version-badge.js +1 -1
- 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/curation-install.js +2 -2
- 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 +45 -0
- package/src/assets/agents/code.mds +18 -20
- package/src/assets/agents/curate.md +26 -35
- package/src/assets/agents/debug.mds +22 -50
- 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 +36 -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 +48 -52
- 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 +77 -119
- package/src/assets/commands/plan.mds +27 -57
- package/src/assets/commands/release.mds +4 -62
- package/src/assets/commands/research.mds +9 -55
- 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 +154 -19
- 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 +190 -0
- package/src/assets/scripts/resolve-settings.cjs +14 -9
- package/src/assets/scripts/verify-evidence.cjs +551 -25
- 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 +14 -37
- 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,38 @@
|
|
|
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.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
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
|
|
36
|
+
// `CI INDETERMINATE pr=- head=- waited=0 reason=usage`, with no NEXT line, and makes no
|
|
37
|
+
// gh call. The
|
|
38
|
+
// boundary re-checks the CI line against LINE_RE and the 400-character bound, and the
|
|
39
|
+
// NEXT line against the fixed set NEXT_LINES, before it writes them. A wait that fails
|
|
40
|
+
// inside the script prints its INDETERMINATE line (`gh-failed` or `unparseable`) and,
|
|
41
|
+
// with the two flags, the NEXT line of an INDETERMINATE result that waited 0 seconds.
|
|
42
|
+
// Exit 0 whenever a line was printed; a reader treats a missing or unparseable line, an
|
|
43
|
+
// unlisted status or a non-zero exit as INDETERMINATE.
|
|
30
44
|
//
|
|
31
45
|
// Design constraints (binding). The text of each verdict rule is written once, at the
|
|
32
46
|
// marker named below; this list points to it. The workflow-probe rule has two readers,
|
|
@@ -49,8 +63,11 @@
|
|
|
49
63
|
// way. A silent foreground node call of about 570 s under a Bash timeout of
|
|
50
64
|
// 600000 ms completed in a main-thread `claude -p` probe on 2026-10-09.
|
|
51
65
|
// - D-CI-NO-EXIT: nothing here calls process.exit(). An explicit exit can
|
|
52
|
-
// truncate stdout when it is a pipe, and the
|
|
66
|
+
// truncate stdout when it is a pipe, and the printed lines are the whole result. The
|
|
53
67
|
// boundary sets process.exitCode and lets the event loop drain.
|
|
68
|
+
// - D-CI-NEXT: see nextAction. The gate table is plumbing, not judgment: it maps a
|
|
69
|
+
// classified status and two counts to one action and one fixed phrase. The phrases
|
|
70
|
+
// carry no gh output, so no check name reaches the NEXT line.
|
|
54
71
|
// - D-CI-WAIT-BOUNDS: every loop has a fixed bound (MAX_POLLS, the deadline, the
|
|
55
72
|
// name lists), and every wait is capped at CI_WAIT_MAX_SECONDS.
|
|
56
73
|
//
|
|
@@ -160,6 +177,9 @@ const NAME_TOKEN_RE = /^(?:-|\+[0-9]+)$/;
|
|
|
160
177
|
|
|
161
178
|
const PR_RE = /^[1-9][0-9]{0,8}$/;
|
|
162
179
|
const HEAD_RE = /^[0-9a-f]{40}$/;
|
|
180
|
+
/** D-CI-NEXT: the waits run so far, this one included (1 to MAX_WAITS), and the fixes (0 to MAX_FIXES). */
|
|
181
|
+
const WAITS_RE = /^[1-3]$/;
|
|
182
|
+
const FIXES_RE = /^[0-2]$/;
|
|
163
183
|
const NO_PR_RE = /Could not resolve to a PullRequest/;
|
|
164
184
|
const NO_CHECKS_RE = /no checks reported/i;
|
|
165
185
|
|
|
@@ -173,6 +193,35 @@ const LINE_RE = new RegExp(
|
|
|
173
193
|
|
|
174
194
|
const USAGE_LINE = 'CI INDETERMINATE pr=- head=- waited=0 reason=usage';
|
|
175
195
|
|
|
196
|
+
/**
|
|
197
|
+
* D-CI-NEXT: the budget of one CI gate (3 waits and 2 fixes in all), the actions it
|
|
198
|
+
* answers with and the phrases it says. A phrase is fixed text of this file; none holds
|
|
199
|
+
* gh output. `none` says nothing. After a full-deadline wait that ends in one of
|
|
200
|
+
* FINAL_AFTER_FULL_WAIT, another wait cannot change the answer, so the gate stops.
|
|
201
|
+
* WAITS_RE and FIXES_RE spell the same two bounds.
|
|
202
|
+
*/
|
|
203
|
+
const MAX_WAITS = 3;
|
|
204
|
+
const MAX_FIXES = 2;
|
|
205
|
+
const NEXTS = Object.freeze(['wait', 'fix', 'proceed']);
|
|
206
|
+
const FINAL_AFTER_FULL_WAIT = Object.freeze(['head-mismatch', 'no-workflow-check']);
|
|
207
|
+
const SAY_NONE = 'none';
|
|
208
|
+
const SAY_NO_CI = 'No PR/CI configured, skipping CI validation.';
|
|
209
|
+
const SAY_RUNNING = 'CI still running — verify manually before merging';
|
|
210
|
+
const SAY_UNKNOWN = 'CI status unknown — verify manually before merging';
|
|
211
|
+
const SAY_FIX = 'CI failed — a fix runs for the failing checks';
|
|
212
|
+
const SAY_STILL_FAILS = 'CI still fails after ' + MAX_FIXES + ' fixes — verify manually before merging';
|
|
213
|
+
|
|
214
|
+
/** The only NEXT lines this script prints: an action and a phrase that go together. */
|
|
215
|
+
const NEXT_LINES = Object.freeze([
|
|
216
|
+
'NEXT=wait SAY=' + SAY_NONE,
|
|
217
|
+
'NEXT=fix SAY=' + SAY_FIX,
|
|
218
|
+
'NEXT=proceed SAY=' + SAY_NONE,
|
|
219
|
+
'NEXT=proceed SAY=' + SAY_NO_CI,
|
|
220
|
+
'NEXT=proceed SAY=' + SAY_RUNNING,
|
|
221
|
+
'NEXT=proceed SAY=' + SAY_UNKNOWN,
|
|
222
|
+
'NEXT=proceed SAY=' + SAY_STILL_FAILS,
|
|
223
|
+
]);
|
|
224
|
+
|
|
176
225
|
/**
|
|
177
226
|
* D-CI-WORKFLOWS: the probe, a GET of the repository's workflow list. It is a fixed
|
|
178
227
|
* argv with no field flag, no method flag and no input, and it names neither the PR nor
|
|
@@ -204,6 +253,8 @@ const VIEW_FIELDS = 'headRefOid,mergeable,state';
|
|
|
204
253
|
* @typedef {{ status: string, pr: string, head: string, failing: string[], pending: string[],
|
|
205
254
|
* waited: number, reason: string | null }} CiResult
|
|
206
255
|
* `head` is the first 7 hex characters.
|
|
256
|
+
* @typedef {{ next: string, say: string }} NextAction
|
|
257
|
+
* `next` is one of NEXTS; `say` is one fixed phrase or SAY_NONE (D-CI-NEXT).
|
|
207
258
|
* @typedef {{ pr: string, head: string, exec: ExecFn, clock: Clock, env?: NodeJS.ProcessEnv }} WaitInput
|
|
208
259
|
* @typedef {{ state: 'deadline', rowsRead: boolean } | { state: 'failed' } | { state: 'pending', pending: string[] }
|
|
209
260
|
* | { state: 'open' }
|
|
@@ -231,26 +282,34 @@ const DEADLINE_LAST = Object.freeze({ status: 'INDETERMINATE', reason: 'deadline
|
|
|
231
282
|
// ---------------------------------------------------------------------------
|
|
232
283
|
|
|
233
284
|
/**
|
|
234
|
-
* D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in
|
|
235
|
-
* order, and nothing else.
|
|
236
|
-
*
|
|
285
|
+
* D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in any
|
|
286
|
+
* order, and nothing else. D-CI-NEXT adds an opt-in pair, `--waits <1 to 3>` and
|
|
287
|
+
* `--fixes <0 to 2>`: both or neither, each once, a single digit in its range. Anything
|
|
288
|
+
* that fails is refused before any gh call, so a value never reaches gh's argv unless it
|
|
289
|
+
* is a plain number or a plain SHA. The result holds `waits` and `fixes` only when the
|
|
290
|
+
* pair was given.
|
|
237
291
|
*
|
|
238
292
|
* @param {readonly string[]} args the arguments after the script path
|
|
239
|
-
* @returns {{ pr: string, head: string } | null}
|
|
293
|
+
* @returns {{ pr: string, head: string, waits?: number, fixes?: number } | null}
|
|
240
294
|
*/
|
|
241
295
|
function parseArgs(args) {
|
|
242
|
-
if (!Array.isArray(args) || args.length !== 4) return null;
|
|
296
|
+
if (!Array.isArray(args) || (args.length !== 4 && args.length !== 8)) return null;
|
|
243
297
|
let pr = null;
|
|
244
298
|
let head = null;
|
|
245
|
-
|
|
299
|
+
let waits = null;
|
|
300
|
+
let fixes = null;
|
|
301
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
246
302
|
const flag = args[i];
|
|
247
303
|
const value = args[i + 1];
|
|
248
304
|
if (typeof value !== 'string') return null;
|
|
249
305
|
if (flag === '--pr' && pr === null && PR_RE.test(value)) pr = value;
|
|
250
306
|
else if (flag === '--head' && head === null && HEAD_RE.test(value)) head = value;
|
|
307
|
+
else if (flag === '--waits' && waits === null && WAITS_RE.test(value)) waits = Number(value);
|
|
308
|
+
else if (flag === '--fixes' && fixes === null && FIXES_RE.test(value)) fixes = Number(value);
|
|
251
309
|
else return null;
|
|
252
310
|
}
|
|
253
|
-
|
|
311
|
+
if (pr === null || head === null) return null;
|
|
312
|
+
return waits === null || fixes === null ? { pr, head } : { pr, head, waits, fixes };
|
|
254
313
|
}
|
|
255
314
|
|
|
256
315
|
// ---------------------------------------------------------------------------
|
|
@@ -364,6 +423,63 @@ function checkLine(line) {
|
|
|
364
423
|
return m !== null && (m[1] === 'INDETERMINATE') === (m[4] !== undefined);
|
|
365
424
|
}
|
|
366
425
|
|
|
426
|
+
/**
|
|
427
|
+
* D-CI-NEXT: what the CI gate does with one printed result. The table is the one the
|
|
428
|
+
* /implement and /resolve prompts used to carry:
|
|
429
|
+
* PASSING proceed, saying nothing
|
|
430
|
+
* NO_PR, NO_CI proceed, saying that no CI is configured
|
|
431
|
+
* PENDING wait while fewer than MAX_WAITS waits have run, else proceed
|
|
432
|
+
* INDETERMINATE wait while fewer than MAX_WAITS waits have run, unless the
|
|
433
|
+
* wait ran the whole deadline and the reason is one of
|
|
434
|
+
* FINAL_AFTER_FULL_WAIT; else proceed, saying the status is unknown
|
|
435
|
+
* FAILING fix while fewer than MAX_FIXES fixes have run, else proceed
|
|
436
|
+
* A status outside STATUSES is unknown, which fails closed to the same proceed.
|
|
437
|
+
* `waited` and `reason` are the result's own values, never read back from text.
|
|
438
|
+
*
|
|
439
|
+
* @param {{ status: string, reason: string | null, waited: number }} result
|
|
440
|
+
* @param {number} waits the waits run so far, this one included
|
|
441
|
+
* @param {number} fixes the Fix agents run so far
|
|
442
|
+
* @returns {NextAction}
|
|
443
|
+
*/
|
|
444
|
+
function nextAction(result, waits, fixes) {
|
|
445
|
+
const proceed = say => ({ next: 'proceed', say });
|
|
446
|
+
switch (result.status) {
|
|
447
|
+
case 'PASSING':
|
|
448
|
+
return proceed(SAY_NONE);
|
|
449
|
+
case 'NO_PR':
|
|
450
|
+
case 'NO_CI':
|
|
451
|
+
return proceed(SAY_NO_CI);
|
|
452
|
+
case 'PENDING':
|
|
453
|
+
return waits < MAX_WAITS ? { next: 'wait', say: SAY_NONE } : proceed(SAY_RUNNING);
|
|
454
|
+
case 'INDETERMINATE': {
|
|
455
|
+
const final = result.waited === CI_WAIT_MAX_SECONDS && FINAL_AFTER_FULL_WAIT.includes(result.reason);
|
|
456
|
+
return waits < MAX_WAITS && !final ? { next: 'wait', say: SAY_NONE } : proceed(SAY_UNKNOWN);
|
|
457
|
+
}
|
|
458
|
+
case 'FAILING':
|
|
459
|
+
return fixes < MAX_FIXES ? { next: 'fix', say: SAY_FIX } : proceed(SAY_STILL_FAILS);
|
|
460
|
+
default:
|
|
461
|
+
return proceed(SAY_UNKNOWN);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {NextAction} action
|
|
467
|
+
* @returns {string} `NEXT=<action> SAY=<message>`
|
|
468
|
+
*/
|
|
469
|
+
function renderNext(action) {
|
|
470
|
+
return 'NEXT=' + action.next + ' SAY=' + action.say;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Whether a line is one this script may print as the NEXT line: one of NEXT_LINES, whole.
|
|
475
|
+
*
|
|
476
|
+
* @param {string} line
|
|
477
|
+
* @returns {boolean}
|
|
478
|
+
*/
|
|
479
|
+
function checkNext(line) {
|
|
480
|
+
return typeof line === 'string' && NEXT_LINES.includes(line);
|
|
481
|
+
}
|
|
482
|
+
|
|
367
483
|
// ---------------------------------------------------------------------------
|
|
368
484
|
// One bounded gh call
|
|
369
485
|
// ---------------------------------------------------------------------------
|
|
@@ -947,9 +1063,10 @@ async function main(argv, deps) {
|
|
|
947
1063
|
/** @param {string} reason */
|
|
948
1064
|
const indeterminate = reason =>
|
|
949
1065
|
'CI INDETERMINATE pr=' + parsed.pr + ' head=' + parsed.head.slice(0, 7) + ' waited=0 reason=' + reason;
|
|
1066
|
+
let result = null;
|
|
950
1067
|
let line;
|
|
951
1068
|
try {
|
|
952
|
-
|
|
1069
|
+
result = await waitForCi({
|
|
953
1070
|
pr: parsed.pr,
|
|
954
1071
|
head: parsed.head,
|
|
955
1072
|
exec: injected.exec || defaultExec,
|
|
@@ -958,10 +1075,20 @@ async function main(argv, deps) {
|
|
|
958
1075
|
});
|
|
959
1076
|
line = renderLine(result);
|
|
960
1077
|
} catch (_) {
|
|
1078
|
+
result = null;
|
|
961
1079
|
line = indeterminate('gh-failed');
|
|
962
1080
|
}
|
|
963
|
-
if (!checkLine(line))
|
|
964
|
-
|
|
1081
|
+
if (!checkLine(line)) {
|
|
1082
|
+
result = null;
|
|
1083
|
+
line = indeterminate('unparseable');
|
|
1084
|
+
}
|
|
1085
|
+
if (parsed.waits === undefined || parsed.fixes === undefined) return { code: 0, stdout: line + '\n' };
|
|
1086
|
+
// D-CI-NEXT: a wait that failed inside this script is INDETERMINATE that waited 0 seconds,
|
|
1087
|
+
// so the gate waits again while waits remain, as the prompts did before the NEXT line.
|
|
1088
|
+
const failed = { status: 'INDETERMINATE', reason: null, waited: 0 };
|
|
1089
|
+
const next = renderNext(nextAction(result === null ? failed : result, parsed.waits, parsed.fixes));
|
|
1090
|
+
if (!checkNext(next)) return { code: 0, stdout: indeterminate('unparseable') + '\n' };
|
|
1091
|
+
return { code: 0, stdout: line + '\n' + next + '\n' };
|
|
965
1092
|
}
|
|
966
1093
|
|
|
967
1094
|
// ---------------------------------------------------------------------------
|
|
@@ -1011,6 +1138,11 @@ module.exports = Object.freeze({
|
|
|
1011
1138
|
NAMES_LISTED,
|
|
1012
1139
|
LINE_RE,
|
|
1013
1140
|
USAGE_LINE,
|
|
1141
|
+
MAX_WAITS,
|
|
1142
|
+
MAX_FIXES,
|
|
1143
|
+
NEXTS,
|
|
1144
|
+
FINAL_AFTER_FULL_WAIT,
|
|
1145
|
+
NEXT_LINES,
|
|
1014
1146
|
WORKFLOWS_ARGS,
|
|
1015
1147
|
parseArgs,
|
|
1016
1148
|
sanitizeName,
|
|
@@ -1024,6 +1156,9 @@ module.exports = Object.freeze({
|
|
|
1024
1156
|
askWorkflows,
|
|
1025
1157
|
renderLine,
|
|
1026
1158
|
checkLine,
|
|
1159
|
+
nextAction,
|
|
1160
|
+
renderNext,
|
|
1161
|
+
checkNext,
|
|
1027
1162
|
waitForCi,
|
|
1028
1163
|
main,
|
|
1029
1164
|
});
|
|
@@ -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,190 @@
|
|
|
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
|
+
// and a maxBuffer, with no time limit (D-ROOT-NO-TIMEOUT): main() clears the limit
|
|
41
|
+
// that resolve-settings.cjs's runGit sets, and resolve-settings.cjs keeps its own
|
|
42
|
+
// - a local read only: no network, nothing written
|
|
43
|
+
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
const fs = require('fs');
|
|
47
|
+
const path = require('path');
|
|
48
|
+
const settings = require('./resolve-settings.cjs');
|
|
49
|
+
|
|
50
|
+
const EXIT_CODES = Object.freeze({
|
|
51
|
+
RESOLVED: 0,
|
|
52
|
+
USAGE: 1,
|
|
53
|
+
INPUT_UNUSABLE: 2,
|
|
54
|
+
INTERNAL_ERROR: 4,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const USAGE = 'Usage: node resolve-root.cjs [<dir>]';
|
|
58
|
+
|
|
59
|
+
/** Any C0 control character: a path that holds one cannot be a one-line answer. */
|
|
60
|
+
const CONTROL_RE = /[\x00-\x1f]/;
|
|
61
|
+
|
|
62
|
+
/** The grammar of a successful answer: two absolute paths, one per line, no control character. */
|
|
63
|
+
const OUTPUT_RE = /^WORKTREE=\/[^\x00-\x1f]*\nLEDGER=\/[^\x00-\x1f]*$/;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {readonly string[]} argv process.argv
|
|
67
|
+
* @returns {{ kind: 'resolve', dir: string } | { kind: 'usage', usage: string }}
|
|
68
|
+
*/
|
|
69
|
+
function parseArgs(argv) {
|
|
70
|
+
const rest = Array.isArray(argv) ? argv.slice(2) : [];
|
|
71
|
+
/** @type {string[]} */
|
|
72
|
+
const positionals = [];
|
|
73
|
+
for (const arg of rest) {
|
|
74
|
+
if (typeof arg !== 'string' || arg.startsWith('-')) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'usage',
|
|
77
|
+
usage: 'resolve-root: unrecognised argument ' + JSON.stringify(String(arg)).slice(0, 80) + '\n' + USAGE,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
positionals.push(arg);
|
|
81
|
+
}
|
|
82
|
+
if (positionals.length > 1) return { kind: 'usage', usage: USAGE };
|
|
83
|
+
return { kind: 'resolve', dir: positionals.length === 1 ? positionals[0] : process.cwd() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} dir
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
function isUsableDirectory(dir) {
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(dir).isDirectory();
|
|
93
|
+
} catch (_) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Print one reason line and return the failure outcome.
|
|
100
|
+
*
|
|
101
|
+
* @param {number} code
|
|
102
|
+
* @param {string} reason
|
|
103
|
+
* @returns {{ code: number, line: string }}
|
|
104
|
+
*/
|
|
105
|
+
function fail(code, reason) {
|
|
106
|
+
process.stderr.write('resolve-root: ' + reason + ' — failing closed\n');
|
|
107
|
+
return { code, line: '' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {readonly string[]} argv process.argv
|
|
112
|
+
* @param {{ exec?: (file: string, args: string[], opts: object) => unknown }} [deps] tests only
|
|
113
|
+
* @returns {{ code: number, line: string }}
|
|
114
|
+
*/
|
|
115
|
+
function main(argv, deps) {
|
|
116
|
+
try {
|
|
117
|
+
const base = deps && typeof deps.exec === 'function' ? deps.exec : settings.defaultExec;
|
|
118
|
+
// D-ROOT-NO-TIMEOUT: git runs with no devflow time limit, as the Bash git call of the
|
|
119
|
+
// prompts did; the Bash tool's own timeout bounds the call. runGit's limit is removed here.
|
|
120
|
+
const exec = (file, gitArgs, opts) => base(file, gitArgs, { ...opts, timeout: undefined });
|
|
121
|
+
const args = parseArgs(argv);
|
|
122
|
+
if (args.kind === 'usage') {
|
|
123
|
+
process.stderr.write(args.usage + '\n');
|
|
124
|
+
return { code: EXIT_CODES.USAGE, line: '' };
|
|
125
|
+
}
|
|
126
|
+
const dir = path.resolve(args.dir);
|
|
127
|
+
if (CONTROL_RE.test(dir)) return fail(EXIT_CODES.INPUT_UNUSABLE, 'the directory path holds a control character');
|
|
128
|
+
if (!isUsableDirectory(dir)) {
|
|
129
|
+
return fail(EXIT_CODES.INPUT_UNUSABLE, 'not a usable directory: ' + JSON.stringify(dir).slice(0, 300));
|
|
130
|
+
}
|
|
131
|
+
const toplevel = settings.gitToplevel(exec, dir);
|
|
132
|
+
if (toplevel.kind === 'unknown') return fail(EXIT_CODES.INTERNAL_ERROR, 'git did not answer');
|
|
133
|
+
let worktree = dir;
|
|
134
|
+
let ledger = dir;
|
|
135
|
+
if (toplevel.kind === 'root') {
|
|
136
|
+
worktree = toplevel.root;
|
|
137
|
+
const found = settings.ledgerRootOf(toplevel.root, { exec });
|
|
138
|
+
ledger = found.kind === 'root' ? found.root : toplevel.root;
|
|
139
|
+
}
|
|
140
|
+
if (CONTROL_RE.test(worktree) || CONTROL_RE.test(ledger)) {
|
|
141
|
+
return fail(EXIT_CODES.INPUT_UNUSABLE, 'a root path holds a control character');
|
|
142
|
+
}
|
|
143
|
+
return { code: EXIT_CODES.RESOLVED, line: 'WORKTREE=' + worktree + '\nLEDGER=' + ledger };
|
|
144
|
+
} catch (_) {
|
|
145
|
+
return fail(EXIT_CODES.INTERNAL_ERROR, 'internal error');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Settle whatever main() returned into what the boundary may print: a success carries
|
|
151
|
+
* a line that passes the grammar, and every other outcome prints nothing.
|
|
152
|
+
*
|
|
153
|
+
* @param {unknown} outcome
|
|
154
|
+
* @returns {{ code: number, line: string }}
|
|
155
|
+
*/
|
|
156
|
+
function settleOutcome(outcome) {
|
|
157
|
+
const o = /** @type {any} */ (outcome);
|
|
158
|
+
if (o === null || typeof o !== 'object' || typeof o.line !== 'string' || typeof o.code !== 'number') {
|
|
159
|
+
return { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
160
|
+
}
|
|
161
|
+
if (o.code === EXIT_CODES.RESOLVED) {
|
|
162
|
+
return OUTPUT_RE.test(o.line) ? { code: o.code, line: o.line } : { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
163
|
+
}
|
|
164
|
+
return { code: o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE ? o.code : EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Top-level boundary — the ONLY stdout write and the ONLY exitCode assignment
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
if (require.main === module) {
|
|
172
|
+
let outcome;
|
|
173
|
+
try {
|
|
174
|
+
outcome = main(process.argv);
|
|
175
|
+
} catch (_) {
|
|
176
|
+
process.stderr.write('resolve-root: internal error — failing closed\n');
|
|
177
|
+
outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
|
|
178
|
+
}
|
|
179
|
+
const settled = settleOutcome(outcome);
|
|
180
|
+
if (settled.line !== '') process.stdout.write(settled.line + '\n');
|
|
181
|
+
process.exitCode = settled.code;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = Object.freeze({
|
|
185
|
+
EXIT_CODES,
|
|
186
|
+
OUTPUT_RE,
|
|
187
|
+
parseArgs,
|
|
188
|
+
settleOutcome,
|
|
189
|
+
main,
|
|
190
|
+
});
|