devflow-kit 3.1.0 → 3.3.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 +52 -0
- package/README.md +2 -2
- package/dist/cli/agents-view/render.js +69 -15
- package/dist/cli/agents-view/state.js +40 -14
- package/dist/cli/commands/agents.js +135 -45
- package/dist/cli/commands/init.js +128 -53
- package/dist/cli/commands/learning.js +61 -13
- package/dist/cli/commands/memory.js +35 -14
- package/dist/cli/commands/uninstall.js +163 -39
- package/dist/commands/code-review.md +1 -3
- package/dist/commands/debug.md +15 -12
- package/dist/commands/dynamic-build.md +172 -135
- package/dist/commands/dynamic-plan.md +9 -3
- package/dist/commands/explore.md +10 -4
- package/dist/commands/implement.md +149 -145
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +8 -2
- package/dist/commands/research.md +8 -2
- package/dist/commands/resolve.md +28 -19
- package/dist/commands/self-review.md +16 -13
- package/dist/core/agent-frontmatter.js +25 -0
- package/dist/core/agent-models.js +201 -36
- package/dist/core/agent-state.js +27 -5
- package/dist/core/assets.js +1 -1
- package/dist/core/feature-config.js +68 -10
- package/dist/core/flags.js +24 -0
- package/dist/core/learning-queue-cleanup.js +10 -11
- package/dist/core/learning-tuning-config.js +8 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/plugins.js +16 -5
- package/dist/core/queue-drain.js +31 -0
- package/dist/hud/components/learning-counts.js +54 -8
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
- package/dist/targets/claude-code/installer.js +36 -9
- package/dist/targets/claude-code/post-install.js +128 -38
- package/package.json +1 -1
- package/src/assets/agents/code.md +85 -35
- package/src/assets/agents/design.md +12 -0
- package/src/assets/agents/diagnose.md +18 -11
- package/src/assets/agents/evaluate.md +17 -24
- package/src/assets/agents/knowledge.md +7 -3
- package/src/assets/agents/learning.md +4 -6
- package/src/assets/agents/research.md +21 -0
- package/src/assets/agents/review.md +12 -0
- package/src/assets/agents/scrutinize.md +37 -9
- package/src/assets/agents/simplify.md +24 -0
- package/src/assets/agents/skim.md +6 -2
- package/src/assets/agents/synthesize.md +18 -0
- package/src/assets/agents/test.md +19 -11
- package/src/assets/agents/triage.md +8 -0
- package/src/assets/agents/validate.md +20 -11
- package/src/assets/commands/_partials/_engine.mds +36 -55
- package/src/assets/commands/_partials/_knowledge.mds +1 -3
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/_partials/_wave.mds +8 -6
- package/src/assets/commands/code-review.mds +1 -3
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +126 -72
- package/src/assets/commands/dynamic-plan.mds +7 -1
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +147 -141
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +8 -2
- package/src/assets/commands/research.mds +8 -2
- package/src/assets/commands/resolve.mds +27 -16
- package/src/assets/commands/self-review.mds +15 -10
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +2 -2
- package/src/assets/mds/tracker/_jira.mds +2 -2
- package/src/assets/mds/tracker/_linear.mds +2 -2
- package/src/assets/scripts/ci-wait.cjs +636 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +356 -17
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +4 -3
- package/src/assets/scripts/hooks/ensure-devflow-init +13 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +122 -10
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/json-helper.cjs +12 -145
- package/src/assets/scripts/hooks/json-parse +24 -129
- package/src/assets/scripts/hooks/lib/learning-store.cjs +169 -64
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +53 -21
- package/src/assets/scripts/hooks/session-start-context +108 -29
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +9 -12
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +15 -33
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/assets/scripts/ci-wait.cjs
|
|
3
|
+
//
|
|
4
|
+
// The CI wait for one pushed head (Token Diet wave 2, #421). /implement Phase 9 and
|
|
5
|
+
// /resolve Phase 8 call it inline, through the Bash tool, instead of re-spawning the
|
|
6
|
+
// Git agent once a minute: a poll costs no model tokens, so the wait happens here.
|
|
7
|
+
// It reads CI with `gh pr view` and `gh pr checks` only, classifies the head it was
|
|
8
|
+
// told about, and prints exactly one line. Installed beside hud.sh and
|
|
9
|
+
// redact-secrets.cjs under ~/.devflow/scripts/.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node ci-wait.cjs --pr <n> --head <sha>
|
|
13
|
+
// <n> the pull request number: digits, no leading zero, at most 9
|
|
14
|
+
// <sha> the pushed head: 40 lowercase hex
|
|
15
|
+
//
|
|
16
|
+
// Run it from the worktree, so gh finds the repository from the working directory.
|
|
17
|
+
//
|
|
18
|
+
// stdout (D-CI-LINE) is exactly one line, ending in a newline:
|
|
19
|
+
// CI <STATUS> pr=<n> head=<first 7 hex> failing=<names|-> pending=<names|-> waited=<seconds>
|
|
20
|
+
// with ` reason=<code>` appended to INDETERMINATE only. STATUS is one of STATUSES,
|
|
21
|
+
// the check-ci-status Output enum. An argv that fails its gate prints the short form
|
|
22
|
+
// `CI INDETERMINATE pr=- head=- waited=0 reason=usage` and makes no gh call. The
|
|
23
|
+
// boundary re-checks the line against LINE_RE and the 400-character bound before it
|
|
24
|
+
// writes it. Exit 0 whenever a line was printed; a reader treats a missing or
|
|
25
|
+
// unparseable line, an unlisted status or a non-zero exit as INDETERMINATE.
|
|
26
|
+
//
|
|
27
|
+
// Design constraints (binding):
|
|
28
|
+
// - D-CI-WAIT-INLINE: this is plumbing. The only judgment in it is the bucket
|
|
29
|
+
// classifier, which mirrors check-ci-status step 5 clause for clause. Every
|
|
30
|
+
// subprocess is `gh`, spawned through exec() with an argv array and
|
|
31
|
+
// `shell: false`, stdin ignored, a timeout and a maxBuffer. gh output never
|
|
32
|
+
// reaches stdout raw: a check name passes sanitizeName() first.
|
|
33
|
+
// - D-CI-NO-LITERAL-SLEEP: the wait happens inside node, through clock.sleep(),
|
|
34
|
+
// never through a shell `sleep`. Claude Code 2.1.294 blocks any Bash command
|
|
35
|
+
// that starts with a literal `sleep N`, so no prompt and no script may wait that
|
|
36
|
+
// way. A silent foreground node call of about 570 s under a Bash timeout of
|
|
37
|
+
// 600000 ms completed in a main-thread `claude -p` probe on 2026-10-09.
|
|
38
|
+
// - D-CI-NO-EXIT: nothing here calls process.exit(). An explicit exit can
|
|
39
|
+
// truncate stdout when it is a pipe, and the one line is the whole result. The
|
|
40
|
+
// boundary sets process.exitCode and lets the event loop drain.
|
|
41
|
+
// - D-CI-WAIT-BOUNDS: every loop has a fixed bound (MAX_POLLS, the deadline, the
|
|
42
|
+
// name lists), and every wait is capped at CI_WAIT_MAX_SECONDS.
|
|
43
|
+
//
|
|
44
|
+
// gh facts this file rests on, probed on gh 2.88.1 on 2026-10-09:
|
|
45
|
+
// - `gh pr view 999999999 --json headRefOid` exits 1 with stderr `GraphQL: Could
|
|
46
|
+
// not resolve to a PullRequest with the number of 999999999.
|
|
47
|
+
// (repository.pullRequest)` and an empty stdout. Only that text on exit 1 is
|
|
48
|
+
// NO_PR; any other non-zero exit is a gh failure and never PASSING.
|
|
49
|
+
// - `gh pr checks <n> --json` offers exactly bucket, completedAt, description,
|
|
50
|
+
// event, link, name, startedAt, state and workflow. There is no SHA field, and it
|
|
51
|
+
// reports only the PR's current head. So the head is bound by comparing
|
|
52
|
+
// `gh pr view --json headRefOid` with `--head` (D-CI-HEAD-BINDING).
|
|
53
|
+
|
|
54
|
+
'use strict';
|
|
55
|
+
|
|
56
|
+
const childProcess = require('child_process');
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Closed vocabularies and bounds
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* D-CI-STATUSES: the check-ci-status Output enum, in its order. A guard in
|
|
64
|
+
* merge-readiness.test.ts holds this list equal to that op's declared one, and
|
|
65
|
+
* reads the arm names of both CI gate blocks against it.
|
|
66
|
+
*/
|
|
67
|
+
const STATUSES = Object.freeze(['PASSING', 'FAILING', 'PENDING', 'NO_CI', 'NO_PR', 'INDETERMINATE']);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* D-CI-REASONS: why an INDETERMINATE was returned. `unrecognised-bucket` also covers
|
|
71
|
+
* a set of buckets that gives no verdict (every check `skipping`): the classifier
|
|
72
|
+
* could call neither a pass nor a failure.
|
|
73
|
+
*/
|
|
74
|
+
const REASONS = Object.freeze([
|
|
75
|
+
'gh-failed', 'unparseable', 'unrecognised-bucket', 'head-mismatch', 'deadline', 'usage',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** `gh pr checks` categorises `state` into these buckets (gh 2.88.1, `--help`). */
|
|
79
|
+
const BUCKETS = Object.freeze(['pass', 'fail', 'pending', 'skipping', 'cancel']);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* D-CI-WAIT-BOUNDS:
|
|
83
|
+
* CI_WAIT_MAX_SECONDS one invocation never runs past this, every gh
|
|
84
|
+
* timeout and every sleep counted; it sits under the
|
|
85
|
+
* Bash timeout of 600000 ms the callers pass
|
|
86
|
+
* POLL_INTERVAL_SECONDS the sleep between polls; polls cost no model tokens,
|
|
87
|
+
* so the cadence is set by gh latency and rate limits
|
|
88
|
+
* SETTLE_WINDOW_SECONDS how long a head mismatch or an empty check list may
|
|
89
|
+
* persist before it is a verdict (GitHub creates
|
|
90
|
+
* checks and moves the PR head a few seconds after a
|
|
91
|
+
* push)
|
|
92
|
+
* GH_CALL_CAP_SECONDS one gh call's timeout is min(this, time remaining),
|
|
93
|
+
* so the last call ends before the deadline
|
|
94
|
+
* MAX_POLLS the loop bound when the clock does not move
|
|
95
|
+
* MAX_CONSECUTIVE_GH_FAILURES failed polls in a row before gh-failed
|
|
96
|
+
*/
|
|
97
|
+
const CI_WAIT_MAX_SECONDS = 570;
|
|
98
|
+
const POLL_INTERVAL_SECONDS = 20;
|
|
99
|
+
const SETTLE_WINDOW_SECONDS = 90;
|
|
100
|
+
const GH_CALL_CAP_SECONDS = 30;
|
|
101
|
+
const MAX_POLLS = 40;
|
|
102
|
+
const MAX_CONSECUTIVE_GH_FAILURES = 3;
|
|
103
|
+
|
|
104
|
+
/** A PR with more check rows than this is read as an unparseable reply. */
|
|
105
|
+
const MAX_CHECKS = 1000;
|
|
106
|
+
/** `gh pr checks --json` for 1,000 rows is far under this. An overflow is a gh failure. */
|
|
107
|
+
const GH_MAX_BUFFER = 4 * 1024 * 1024;
|
|
108
|
+
/** Kept of a gh stderr; only a fixed phrase is ever matched against it. */
|
|
109
|
+
const STDERR_KEEP = 4096;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* D-CI-SANITIZE: the shape of a name on stdout. Names come from a third party's
|
|
113
|
+
* workflow files, so the line carries only an allowlist, 40 characters per name, 4
|
|
114
|
+
* names per list and a `+N` for the rest. Neither a newline, an ANSI escape, a
|
|
115
|
+
* backtick, a comma nor `=` survives, so a name can neither start a second line nor
|
|
116
|
+
* forge a `key=value` field. A name that would read as one of the list's own tokens,
|
|
117
|
+
* `-` (no names) or `+N` (the count of the rest), has its first character turned
|
|
118
|
+
* to `_`, so a FAILING line never reads as naming nothing and a fourth name never
|
|
119
|
+
* reads as a count. Two lists of 4 names at 40 characters, the longest
|
|
120
|
+
* prefix and a three-digit tail come to under 400 characters by construction, and the
|
|
121
|
+
* boundary asserts it.
|
|
122
|
+
*/
|
|
123
|
+
const NAME_MAX = 40;
|
|
124
|
+
const NAMES_LISTED = 4;
|
|
125
|
+
const MORE_MAX = 999;
|
|
126
|
+
const LINE_MAX = 400;
|
|
127
|
+
const NAME_UNSAFE_RE = /[^A-Za-z0-9 ._()\/:+-]/gu;
|
|
128
|
+
/** A sanitized name that spells a list token: `-` (no names) or `+N` (the rest). */
|
|
129
|
+
const NAME_TOKEN_RE = /^(?:-|\+[0-9]+)$/;
|
|
130
|
+
|
|
131
|
+
const PR_RE = /^[1-9][0-9]{0,8}$/;
|
|
132
|
+
const HEAD_RE = /^[0-9a-f]{40}$/;
|
|
133
|
+
const NO_PR_RE = /Could not resolve to a PullRequest/;
|
|
134
|
+
const NO_CHECKS_RE = /no checks reported/i;
|
|
135
|
+
|
|
136
|
+
const NAME_LIST = '(?:-|[A-Za-z0-9 ._()/:+-]{1,40}(?:,[A-Za-z0-9 ._()/:+-]{1,40}){0,3}(?:,\\+[0-9]{1,3})?)';
|
|
137
|
+
/** The one stdout line: the full form, or the usage form without the two name lists. */
|
|
138
|
+
const LINE_RE = new RegExp(
|
|
139
|
+
'^CI (PASSING|FAILING|PENDING|NO_CI|NO_PR|INDETERMINATE) pr=([1-9][0-9]{0,8}|-) head=([0-9a-f]{7}|-)'
|
|
140
|
+
+ '(?: failing=' + NAME_LIST + ' pending=' + NAME_LIST + ')?'
|
|
141
|
+
+ ' waited=[0-9]{1,3}(?: reason=(' + REASONS.join('|') + '))?$',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const USAGE_LINE = 'CI INDETERMINATE pr=- head=- waited=0 reason=usage';
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Types
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @typedef {{ status: number | null, stdout?: Buffer | string, stderr?: Buffer | string,
|
|
152
|
+
* error?: { code?: string } }} ExecResult
|
|
153
|
+
* The spawnSync subset this script reads.
|
|
154
|
+
* @typedef {(file: string, args: string[], opts: object) => ExecResult | Promise<ExecResult>} ExecFn
|
|
155
|
+
* Called like child_process.spawnSync(file, args, opts); a promise is awaited.
|
|
156
|
+
* @typedef {{ now: () => number, sleep: (ms: number) => Promise<void> }} Clock
|
|
157
|
+
* `now` is in milliseconds. Tests inject a clock whose sleep and exec advance it.
|
|
158
|
+
*
|
|
159
|
+
* @typedef {{ name: string, bucket: string }} Check
|
|
160
|
+
* @typedef {{ status: string, reason: string | null, failing: string[], pending: string[] }} Classification
|
|
161
|
+
* @typedef {{ status: string, pr: string, head: string, failing: string[], pending: string[],
|
|
162
|
+
* waited: number, reason: string | null }} CiResult
|
|
163
|
+
* `head` is the first 7 hex characters.
|
|
164
|
+
* @typedef {{ pr: string, head: string, exec: ExecFn, clock: Clock, env?: NodeJS.ProcessEnv }} WaitInput
|
|
165
|
+
* @typedef {{ exec?: ExecFn, clock?: Clock, env?: NodeJS.ProcessEnv }} MainDeps
|
|
166
|
+
* @typedef {{ code: number, stdout: string }} Outcome
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Argument gate
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in either
|
|
175
|
+
* order, and nothing else. Anything that fails is refused before any gh call, so a
|
|
176
|
+
* value never reaches gh's argv unless it is a plain number or a plain SHA.
|
|
177
|
+
*
|
|
178
|
+
* @param {readonly string[]} args the arguments after the script path
|
|
179
|
+
* @returns {{ pr: string, head: string } | null}
|
|
180
|
+
*/
|
|
181
|
+
function parseArgs(args) {
|
|
182
|
+
if (!Array.isArray(args) || args.length !== 4) return null;
|
|
183
|
+
let pr = null;
|
|
184
|
+
let head = null;
|
|
185
|
+
for (let i = 0; i < 4; i += 2) {
|
|
186
|
+
const flag = args[i];
|
|
187
|
+
const value = args[i + 1];
|
|
188
|
+
if (typeof value !== 'string') return null;
|
|
189
|
+
if (flag === '--pr' && pr === null && PR_RE.test(value)) pr = value;
|
|
190
|
+
else if (flag === '--head' && head === null && HEAD_RE.test(value)) head = value;
|
|
191
|
+
else return null;
|
|
192
|
+
}
|
|
193
|
+
return pr !== null && head !== null ? { pr, head } : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Sanitizer, classifier and line
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* D-CI-SANITIZE: one check name as it may appear on stdout.
|
|
202
|
+
*
|
|
203
|
+
* @param {unknown} name
|
|
204
|
+
* @returns {string} 1 to 40 characters from the allowlist
|
|
205
|
+
*/
|
|
206
|
+
function sanitizeName(name) {
|
|
207
|
+
const text = typeof name === 'string' ? name.slice(0, 4 * NAME_MAX) : '';
|
|
208
|
+
const safe = text.replace(NAME_UNSAFE_RE, '_').slice(0, NAME_MAX);
|
|
209
|
+
if (safe === '') return '_';
|
|
210
|
+
return NAME_TOKEN_RE.test(safe) ? '_' + safe.slice(1) : safe;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* One list on the line: at most NAMES_LISTED sanitized names, then `+N` for the rest.
|
|
215
|
+
*
|
|
216
|
+
* @param {readonly string[]} names
|
|
217
|
+
* @returns {string} `-` for an empty list
|
|
218
|
+
*/
|
|
219
|
+
function formatNames(names) {
|
|
220
|
+
if (!Array.isArray(names) || names.length === 0) return '-';
|
|
221
|
+
const shown = names.slice(0, NAMES_LISTED).map(sanitizeName);
|
|
222
|
+
const more = names.length - shown.length;
|
|
223
|
+
return more > 0 ? shown.join(',') + ',+' + Math.min(more, MORE_MAX) : shown.join(',');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* D-CI-CLASSIFIER: check-ci-status step 5, clause for clause, over `bucket`. In
|
|
228
|
+
* priority order: any `pending` is PENDING; else any `fail` or `cancel` is FAILING;
|
|
229
|
+
* else every check `pass` or `skipping` with at least one `pass` is PASSING; else
|
|
230
|
+
* INDETERMINATE. PASSING is a positive conjunction, so an undocumented bucket, an
|
|
231
|
+
* all-`skipping` set or an empty set never reaches it. merge-readiness.test.ts runs
|
|
232
|
+
* every bucket multiset of 1 to 3 checks through this and through the prose clauses
|
|
233
|
+
* and requires the statuses to agree. (The empty set is not classified here: an empty
|
|
234
|
+
* list is "no checks", which waitForCi settles before it classifies.)
|
|
235
|
+
*
|
|
236
|
+
* @param {readonly Check[]} checks
|
|
237
|
+
* @returns {Classification}
|
|
238
|
+
*/
|
|
239
|
+
function classifyChecks(checks) {
|
|
240
|
+
const failing = [];
|
|
241
|
+
const pending = [];
|
|
242
|
+
let pass = 0;
|
|
243
|
+
let unrecognised = 0;
|
|
244
|
+
const count = Math.min(Array.isArray(checks) ? checks.length : 0, MAX_CHECKS);
|
|
245
|
+
for (let i = 0; i < count; i++) {
|
|
246
|
+
const check = checks[i];
|
|
247
|
+
switch (check.bucket) {
|
|
248
|
+
case 'pending': pending.push(check.name); break;
|
|
249
|
+
case 'fail':
|
|
250
|
+
case 'cancel': failing.push(check.name); break;
|
|
251
|
+
case 'pass': pass++; break;
|
|
252
|
+
case 'skipping': break;
|
|
253
|
+
default: unrecognised++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (pending.length > 0) return { status: 'PENDING', reason: null, failing, pending };
|
|
257
|
+
if (failing.length > 0) return { status: 'FAILING', reason: null, failing, pending };
|
|
258
|
+
if (unrecognised === 0 && pass > 0) return { status: 'PASSING', reason: null, failing, pending };
|
|
259
|
+
return { status: 'INDETERMINATE', reason: 'unrecognised-bucket', failing, pending };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* D-CI-LINE: the result as its one line. Only the list a status is about is filled
|
|
264
|
+
* (`failing` on FAILING, `pending` on PENDING), which keeps the line far under the
|
|
265
|
+
* bound; `reason` rides on INDETERMINATE only.
|
|
266
|
+
*
|
|
267
|
+
* @param {CiResult} result
|
|
268
|
+
* @returns {string}
|
|
269
|
+
*/
|
|
270
|
+
function renderLine(result) {
|
|
271
|
+
const failing = result.status === 'FAILING' ? formatNames(result.failing) : '-';
|
|
272
|
+
const pending = result.status === 'PENDING' ? formatNames(result.pending) : '-';
|
|
273
|
+
const reason = result.status === 'INDETERMINATE' && result.reason !== null ? ' reason=' + result.reason : '';
|
|
274
|
+
return 'CI ' + result.status + ' pr=' + result.pr + ' head=' + result.head
|
|
275
|
+
+ ' failing=' + failing + ' pending=' + pending + ' waited=' + result.waited + reason;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Whether a line is one this script may print: the grammar, the length bound, and
|
|
280
|
+
* `reason=` on INDETERMINATE only.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} line
|
|
283
|
+
* @returns {boolean}
|
|
284
|
+
*/
|
|
285
|
+
function checkLine(line) {
|
|
286
|
+
if (typeof line !== 'string' || line.length > LINE_MAX) return false;
|
|
287
|
+
const m = LINE_RE.exec(line);
|
|
288
|
+
return m !== null && (m[1] === 'INDETERMINATE') === (m[4] !== undefined);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// One bounded gh call
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {unknown} value
|
|
297
|
+
* @returns {string}
|
|
298
|
+
*/
|
|
299
|
+
function asText(value) {
|
|
300
|
+
if (typeof value === 'string') return value;
|
|
301
|
+
return Buffer.isBuffer(value) ? value.toString('utf8') : '';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The environment gh gets: the caller's, with prompts off and colour removed.
|
|
306
|
+
*
|
|
307
|
+
* @returns {NodeJS.ProcessEnv}
|
|
308
|
+
*/
|
|
309
|
+
function childEnv() {
|
|
310
|
+
const env = Object.assign({}, process.env, {
|
|
311
|
+
GH_PROMPT_DISABLED: '1',
|
|
312
|
+
GH_NO_UPDATE_NOTIFIER: '1',
|
|
313
|
+
NO_COLOR: '1',
|
|
314
|
+
});
|
|
315
|
+
delete env.GH_FORCE_TTY;
|
|
316
|
+
delete env.CLICOLOR_FORCE;
|
|
317
|
+
return env;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* A normalized gh answer. `answered` is true only when gh ran to completion and
|
|
322
|
+
* exited on its own; a call that never started, timed out or overflowed is NOT
|
|
323
|
+
* KNOWING, and never reads as a permissive answer.
|
|
324
|
+
*
|
|
325
|
+
* @typedef {{ answered: boolean, status: number | null, stdout: string, stderr: string }} GhAnswer
|
|
326
|
+
*
|
|
327
|
+
* @param {ExecResult | null} res
|
|
328
|
+
* @returns {GhAnswer}
|
|
329
|
+
*/
|
|
330
|
+
function normalize(res) {
|
|
331
|
+
const errored = res === null || typeof res !== 'object' || Boolean(res.error);
|
|
332
|
+
const status = !errored && typeof res.status === 'number' ? res.status : null;
|
|
333
|
+
return {
|
|
334
|
+
answered: !errored && status !== null,
|
|
335
|
+
status,
|
|
336
|
+
stdout: errored ? '' : asText(res.stdout),
|
|
337
|
+
stderr: errored ? '' : asText(res.stderr).slice(0, STDERR_KEEP),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Reply parsers
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {string} text
|
|
347
|
+
* @returns {unknown} the parsed value, or undefined when the text is not JSON
|
|
348
|
+
*/
|
|
349
|
+
function parseJson(text) {
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(text);
|
|
352
|
+
} catch (_) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @param {string} stdout
|
|
359
|
+
* @returns {string | null} the 40-hex headRefOid, or null for any other shape
|
|
360
|
+
*/
|
|
361
|
+
function parseHeadRefOid(stdout) {
|
|
362
|
+
const value = parseJson(stdout);
|
|
363
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
364
|
+
return typeof value.headRefOid === 'string' && HEAD_RE.test(value.headRefOid) ? value.headRefOid : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {string} stdout
|
|
369
|
+
* @returns {Check[] | null} the rows, or null for any other shape
|
|
370
|
+
*/
|
|
371
|
+
function parseChecks(stdout) {
|
|
372
|
+
const value = parseJson(stdout);
|
|
373
|
+
if (!Array.isArray(value) || value.length > MAX_CHECKS) return null;
|
|
374
|
+
const checks = [];
|
|
375
|
+
for (const row of value) {
|
|
376
|
+
if (row === null || typeof row !== 'object' || typeof row.name !== 'string' || typeof row.bucket !== 'string') return null;
|
|
377
|
+
checks.push({ name: row.name, bucket: row.bucket });
|
|
378
|
+
}
|
|
379
|
+
return checks;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// The wait
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* D-CI-HEAD-BINDING: wait for the CI verdict of `head`, and of nothing else. `gh pr
|
|
388
|
+
* checks` has no SHA field and reports the PR's current head, so each poll reads
|
|
389
|
+
* `headRefOid` first. A different value means the checks on offer belong to another
|
|
390
|
+
* head: they are not read, and the poll is PENDING until SETTLE_WINDOW_SECONDS have
|
|
391
|
+
* passed since the first mismatch (GitHub moves the head and creates checks a few
|
|
392
|
+
* seconds after a push), then INDETERMINATE (head-mismatch). A matching head clears
|
|
393
|
+
* the mismatch clock. An empty check list settles the same way into NO_CI. Nothing
|
|
394
|
+
* here is PASSING unless a poll saw this head and every check a pass.
|
|
395
|
+
*
|
|
396
|
+
* Per poll: `gh pr view <n> --json headRefOid`, then `gh pr checks <n> --json
|
|
397
|
+
* name,state,bucket` (exit 0 or 8 are results, as in check-ci-status step 3). A
|
|
398
|
+
* PENDING poll sleeps min(POLL_INTERVAL_SECONDS, remaining) and polls again; any
|
|
399
|
+
* other result returns. A poll whose gh call fails is not a result: three failed
|
|
400
|
+
* polls in a row are INDETERMINATE (gh-failed), fewer are retried. The loop stops at
|
|
401
|
+
* the deadline or after MAX_POLLS with its last state: PENDING stays PENDING, and
|
|
402
|
+
* anything unknown becomes INDETERMINATE (deadline).
|
|
403
|
+
*
|
|
404
|
+
* It never throws: an exec that throws is a gh failure.
|
|
405
|
+
*
|
|
406
|
+
* @param {WaitInput} input
|
|
407
|
+
* @returns {Promise<CiResult>}
|
|
408
|
+
*/
|
|
409
|
+
async function waitForCi(input) {
|
|
410
|
+
const { pr, head, exec, clock } = input;
|
|
411
|
+
const env = input.env || childEnv();
|
|
412
|
+
const startedAt = clock.now();
|
|
413
|
+
const deadline = startedAt + CI_WAIT_MAX_SECONDS * 1000;
|
|
414
|
+
const settleMs = SETTLE_WINDOW_SECONDS * 1000;
|
|
415
|
+
|
|
416
|
+
/** @returns {CiResult} */
|
|
417
|
+
const finish = (status, reason, failing, pending) => ({
|
|
418
|
+
status,
|
|
419
|
+
pr,
|
|
420
|
+
head: head.slice(0, 7),
|
|
421
|
+
failing,
|
|
422
|
+
pending,
|
|
423
|
+
waited: Math.min(CI_WAIT_MAX_SECONDS, Math.max(0, Math.round((clock.now() - startedAt) / 1000))),
|
|
424
|
+
reason,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* One gh call, its timeout min(GH_CALL_CAP_SECONDS, time left); null when no time
|
|
429
|
+
* is left, in which case no call is made.
|
|
430
|
+
*
|
|
431
|
+
* @param {string[]} args
|
|
432
|
+
* @returns {Promise<GhAnswer | null>}
|
|
433
|
+
*/
|
|
434
|
+
const gh = async args => {
|
|
435
|
+
const left = Math.floor(deadline - clock.now());
|
|
436
|
+
if (left < 1) return null;
|
|
437
|
+
let res = null;
|
|
438
|
+
try {
|
|
439
|
+
res = await exec('gh', [...args], {
|
|
440
|
+
env,
|
|
441
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
442
|
+
timeout: Math.min(GH_CALL_CAP_SECONDS * 1000, left),
|
|
443
|
+
maxBuffer: GH_MAX_BUFFER,
|
|
444
|
+
windowsHide: true,
|
|
445
|
+
shell: false,
|
|
446
|
+
});
|
|
447
|
+
} catch (_) {
|
|
448
|
+
res = null;
|
|
449
|
+
}
|
|
450
|
+
return normalize(res);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
let failedPolls = 0;
|
|
454
|
+
let firstMismatchAt = null;
|
|
455
|
+
let firstEmptyAt = null;
|
|
456
|
+
/** @type {{ status: string, pending: string[] }} */
|
|
457
|
+
let last = { status: 'INDETERMINATE', pending: [] };
|
|
458
|
+
|
|
459
|
+
for (let poll = 0; poll < MAX_POLLS; poll++) {
|
|
460
|
+
if (clock.now() >= deadline) break;
|
|
461
|
+
|
|
462
|
+
const view = await gh(['pr', 'view', pr, '--json', 'headRefOid']);
|
|
463
|
+
if (view === null) break;
|
|
464
|
+
if (view.answered && view.status === 1 && NO_PR_RE.test(view.stderr)) return finish('NO_PR', null, [], []);
|
|
465
|
+
|
|
466
|
+
/** @type {{ state: 'failed' } | { state: 'pending', pending: string[] } | { state: 'done', result: CiResult }} */
|
|
467
|
+
let step;
|
|
468
|
+
if (!view.answered || view.status !== 0) {
|
|
469
|
+
step = { state: 'failed' };
|
|
470
|
+
} else {
|
|
471
|
+
const oid = parseHeadRefOid(view.stdout);
|
|
472
|
+
if (oid === null) return finish('INDETERMINATE', 'unparseable', [], []);
|
|
473
|
+
if (oid !== head) {
|
|
474
|
+
firstMismatchAt = firstMismatchAt === null ? clock.now() : firstMismatchAt;
|
|
475
|
+
step = clock.now() - firstMismatchAt >= settleMs
|
|
476
|
+
? { state: 'done', result: finish('INDETERMINATE', 'head-mismatch', [], []) }
|
|
477
|
+
: { state: 'pending', pending: [] };
|
|
478
|
+
} else {
|
|
479
|
+
firstMismatchAt = null;
|
|
480
|
+
const checks = await gh(['pr', 'checks', pr, '--json', 'name,state,bucket']);
|
|
481
|
+
if (checks === null) break;
|
|
482
|
+
const isResult = checks.answered && (checks.status === 0 || checks.status === 8);
|
|
483
|
+
const isEmpty = checks.answered && checks.status === 1 && NO_CHECKS_RE.test(checks.stderr);
|
|
484
|
+
if (!isResult && !isEmpty) {
|
|
485
|
+
step = { state: 'failed' };
|
|
486
|
+
} else {
|
|
487
|
+
const rows = isResult ? parseChecks(checks.stdout) : [];
|
|
488
|
+
if (rows === null) return finish('INDETERMINATE', 'unparseable', [], []);
|
|
489
|
+
if (rows.length === 0) {
|
|
490
|
+
firstEmptyAt = firstEmptyAt === null ? clock.now() : firstEmptyAt;
|
|
491
|
+
step = clock.now() - firstEmptyAt >= settleMs
|
|
492
|
+
? { state: 'done', result: finish('NO_CI', null, [], []) }
|
|
493
|
+
: { state: 'pending', pending: [] };
|
|
494
|
+
} else {
|
|
495
|
+
firstEmptyAt = null;
|
|
496
|
+
const verdict = classifyChecks(rows);
|
|
497
|
+
step = verdict.status === 'PENDING'
|
|
498
|
+
? { state: 'pending', pending: verdict.pending }
|
|
499
|
+
: { state: 'done', result: finish(verdict.status, verdict.reason, verdict.failing, verdict.pending) };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (step.state === 'done') return step.result;
|
|
506
|
+
if (step.state === 'failed') {
|
|
507
|
+
failedPolls++;
|
|
508
|
+
if (failedPolls >= MAX_CONSECUTIVE_GH_FAILURES) return finish('INDETERMINATE', 'gh-failed', [], []);
|
|
509
|
+
last = { status: 'INDETERMINATE', pending: [] };
|
|
510
|
+
} else {
|
|
511
|
+
failedPolls = 0;
|
|
512
|
+
last = { status: 'PENDING', pending: step.pending };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const left = deadline - clock.now();
|
|
516
|
+
if (left <= 0) break;
|
|
517
|
+
await clock.sleep(Math.min(POLL_INTERVAL_SECONDS * 1000, left));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return last.status === 'PENDING'
|
|
521
|
+
? finish('PENDING', null, [], last.pending)
|
|
522
|
+
: finish('INDETERMINATE', 'deadline', [], []);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// main — returns {code, stdout}; never calls process.exit (D-CI-NO-EXIT)
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The production exec: spawnSync, read off the module object at call time.
|
|
531
|
+
*
|
|
532
|
+
* @type {ExecFn}
|
|
533
|
+
*/
|
|
534
|
+
function defaultExec(file, args, opts) {
|
|
535
|
+
return childProcess.spawnSync(file, args, /** @type {any} */ (opts));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The production clock: wall time and a timer. The timer is what keeps the process
|
|
540
|
+
* alive between polls, in place of a shell `sleep` (D-CI-NO-LITERAL-SLEEP).
|
|
541
|
+
*
|
|
542
|
+
* @returns {Clock}
|
|
543
|
+
*/
|
|
544
|
+
function realClock() {
|
|
545
|
+
return {
|
|
546
|
+
now: () => Date.now(),
|
|
547
|
+
sleep: ms => new Promise(resolve => { setTimeout(resolve, ms); }),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {readonly string[]} argv process.argv
|
|
553
|
+
* @param {MainDeps} [deps] injected by tests only
|
|
554
|
+
* @returns {Promise<Outcome>}
|
|
555
|
+
*/
|
|
556
|
+
async function main(argv, deps) {
|
|
557
|
+
const injected = deps || {};
|
|
558
|
+
const parsed = parseArgs(Array.isArray(argv) ? argv.slice(2) : []);
|
|
559
|
+
if (parsed === null) return { code: 0, stdout: USAGE_LINE + '\n' };
|
|
560
|
+
/** @param {string} reason */
|
|
561
|
+
const indeterminate = reason =>
|
|
562
|
+
'CI INDETERMINATE pr=' + parsed.pr + ' head=' + parsed.head.slice(0, 7) + ' waited=0 reason=' + reason;
|
|
563
|
+
let line;
|
|
564
|
+
try {
|
|
565
|
+
const result = await waitForCi({
|
|
566
|
+
pr: parsed.pr,
|
|
567
|
+
head: parsed.head,
|
|
568
|
+
exec: injected.exec || defaultExec,
|
|
569
|
+
clock: injected.clock || realClock(),
|
|
570
|
+
env: injected.env,
|
|
571
|
+
});
|
|
572
|
+
line = renderLine(result);
|
|
573
|
+
} catch (_) {
|
|
574
|
+
line = indeterminate('gh-failed');
|
|
575
|
+
}
|
|
576
|
+
if (!checkLine(line)) line = indeterminate('unparseable');
|
|
577
|
+
return { code: 0, stdout: line + '\n' };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// Top-level boundary — the ONLY stdout write and exitCode assignment
|
|
582
|
+
//
|
|
583
|
+
// A write that fails (a closed pipe, an unwritable descriptor) sets exit 3,
|
|
584
|
+
// whether it fails synchronously, through the write callback, or as a stream
|
|
585
|
+
// 'error' event.
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @param {Outcome} outcome
|
|
590
|
+
* @returns {void}
|
|
591
|
+
*/
|
|
592
|
+
function emit(outcome) {
|
|
593
|
+
process.exitCode = outcome.code;
|
|
594
|
+
const writeFailed = () => { process.exitCode = 3; };
|
|
595
|
+
process.stdout.on('error', writeFailed);
|
|
596
|
+
try {
|
|
597
|
+
process.stdout.write(outcome.stdout, err => { if (err) writeFailed(); });
|
|
598
|
+
} catch (_) {
|
|
599
|
+
writeFailed();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (require.main === module) {
|
|
604
|
+
main(process.argv).then(emit, () => {
|
|
605
|
+
emit({ code: 0, stdout: 'CI INDETERMINATE pr=- head=- waited=0 reason=unparseable\n' });
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
// Exports — the unit tests and the parity test are the consumers
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
|
|
613
|
+
module.exports = Object.freeze({
|
|
614
|
+
STATUSES,
|
|
615
|
+
REASONS,
|
|
616
|
+
BUCKETS,
|
|
617
|
+
CI_WAIT_MAX_SECONDS,
|
|
618
|
+
POLL_INTERVAL_SECONDS,
|
|
619
|
+
SETTLE_WINDOW_SECONDS,
|
|
620
|
+
GH_CALL_CAP_SECONDS,
|
|
621
|
+
MAX_POLLS,
|
|
622
|
+
MAX_CONSECUTIVE_GH_FAILURES,
|
|
623
|
+
LINE_MAX,
|
|
624
|
+
NAME_MAX,
|
|
625
|
+
NAMES_LISTED,
|
|
626
|
+
LINE_RE,
|
|
627
|
+
USAGE_LINE,
|
|
628
|
+
parseArgs,
|
|
629
|
+
sanitizeName,
|
|
630
|
+
formatNames,
|
|
631
|
+
classifyChecks,
|
|
632
|
+
renderLine,
|
|
633
|
+
checkLine,
|
|
634
|
+
waitForCi,
|
|
635
|
+
main,
|
|
636
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- ORCHESTRATOR CHARTER ---
|
|
2
2
|
You are the orchestrator of this session: you coordinate, agents produce.
|
|
3
3
|
|
|
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.
|
|
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 only a single git, gh or script command whose output stays under about 40 lines, and never a diff, log or test run.
|
|
5
5
|
|
|
6
6
|
Routing (Agent tool) — pick the roster agent that fits the work:
|
|
7
7
|
- Search and listing: Explore. Codebase orientation: Skim.
|
|
8
|
-
- Execution against a spec: Code (write code to a plan
|
|
8
|
+
- Execution against a spec: Code (write code to a plan; open the prompt with `OPERATION: <mode>` — `edit` for mechanical changes (renames, moves, boilerplate), `issue-fix` for pre-classified review issues, else `implement`), Validate (build, typecheck, lint, test), Git (git/GitHub operations).
|
|
9
9
|
- Analysis, design, research: Design, Research, Review, Triage (validate review issues against blast-radius matrix).
|
|
10
10
|
- Real-scale work that matches a workflow: invoke the full skill instead — devflow:implement, devflow:plan, devflow:research, devflow:explore, devflow:debug, devflow:code-review, devflow:resolve.
|
|
11
11
|
|
|
@@ -14,7 +14,8 @@ Stays mainline (judgment work): conversation, decisions, routing, synthesizing a
|
|
|
14
14
|
Operating rules:
|
|
15
15
|
- Decompose mainline. Subagents cannot spawn subagents — you own task breakdown, then delegate leaf tasks.
|
|
16
16
|
- 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 the substance in the prompt — not a pointer to it.
|
|
17
|
+
- Report cap: ask every direct delegation for a final report of at most about 1,500 tokens — findings, paths and verdicts, not file dumps.
|
|
17
18
|
- Parallelize independent delegations in one message. Git operations stay sequential.
|
|
18
19
|
- Feature knowledge (direct delegations only — workflow skills handle their own): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass matching KNOWLEDGE.md content as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
|
|
19
20
|
- Decisions (direct delegations only — workflow skills load their own): pass the index named under PROJECT DECISIONS as DECISIONS_CONTEXT — its content, read once — to every agent that takes it.
|
|
20
|
-
- 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 the
|
|
21
|
+
- 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.
|