devflow-kit 3.9.0 → 3.11.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +1 -1
  3. package/dist/agents/code.md +6 -6
  4. package/dist/agents/fix.md +6 -6
  5. package/dist/agents/harden.md +7 -6
  6. package/dist/agents/refactor.md +6 -6
  7. package/dist/cli/commands/ambient.js +10 -2
  8. package/dist/cli/commands/backup-session.js +131 -0
  9. package/dist/cli/commands/compliance-prompts.js +9 -4
  10. package/dist/cli/commands/compliance.js +184 -24
  11. package/dist/cli/commands/curation.js +23 -10
  12. package/dist/cli/commands/gate-level-prompts.js +39 -25
  13. package/dist/cli/commands/gates.js +5 -7
  14. package/dist/cli/commands/init.js +169 -184
  15. package/dist/cli/commands/install-report.js +129 -0
  16. package/dist/cli/commands/legacy-sweep.js +159 -0
  17. package/dist/cli/commands/proxy.js +6 -1
  18. package/dist/cli/commands/rules.js +75 -21
  19. package/dist/cli/commands/skills.js +42 -9
  20. package/dist/cli/commands/tracker.js +9 -3
  21. package/dist/cli/commands/uninstall.js +339 -75
  22. package/dist/commands/bug-analysis.md +69 -120
  23. package/dist/commands/dynamic-build.md +23 -23
  24. package/dist/commands/implement.md +46 -32
  25. package/dist/commands/release.md +1 -1
  26. package/dist/commands/resolve.md +35 -31
  27. package/dist/commands/self-review.md +8 -7
  28. package/dist/compliance-off/agents/git.md +120 -0
  29. package/dist/compliance-off/agents/triage.md +180 -0
  30. package/dist/compliance-off/commands/bug-analysis.md +398 -0
  31. package/dist/compliance-off/commands/code-review.md +520 -0
  32. package/dist/compliance-off/commands/dynamic-build.md +1251 -0
  33. package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
  34. package/dist/compliance-off/commands/implement.md +736 -0
  35. package/dist/compliance-off/commands/plan.md +671 -0
  36. package/dist/compliance-off/commands/release.md +309 -0
  37. package/dist/compliance-off/commands/resolve.md +742 -0
  38. package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
  39. package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
  40. package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
  41. package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
  42. package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
  43. package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
  44. package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
  45. package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
  46. package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
  47. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
  48. package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
  49. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
  50. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
  51. package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
  52. package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
  53. package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
  54. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
  55. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
  56. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
  57. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
  58. package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
  59. package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
  60. package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
  61. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
  62. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
  63. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
  64. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
  65. package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
  66. package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
  67. package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
  68. package/dist/core/agent-models.js +19 -4
  69. package/dist/core/assets.js +126 -17
  70. package/dist/core/compliance-off-files.js +27 -0
  71. package/dist/core/compliance.js +14 -0
  72. package/dist/core/curation-variants.js +460 -112
  73. package/dist/core/flags.js +2 -2
  74. package/dist/core/fs-atomic.js +5 -1
  75. package/dist/core/installed-variants.js +63 -0
  76. package/dist/core/manifest.js +48 -5
  77. package/dist/core/mds-variants.js +700 -3
  78. package/dist/core/orphan-sweep.js +29 -2
  79. package/dist/core/path-set.js +15 -0
  80. package/dist/core/plugins.js +68 -10
  81. package/dist/core/reference-sweep.js +42 -25
  82. package/dist/core/retired-shipped-names.js +278 -0
  83. package/dist/curation-compliance-off/agents/triage.md +176 -0
  84. package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
  85. package/dist/curation-compliance-off/commands/code-review.md +481 -0
  86. package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
  87. package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
  88. package/dist/curation-compliance-off/commands/implement.md +689 -0
  89. package/dist/curation-compliance-off/commands/plan.md +631 -0
  90. package/dist/curation-compliance-off/commands/release.md +279 -0
  91. package/dist/curation-compliance-off/commands/resolve.md +692 -0
  92. package/dist/curation-off/agents/code.md +6 -6
  93. package/dist/curation-off/agents/fix.md +6 -6
  94. package/dist/curation-off/agents/harden.md +7 -6
  95. package/dist/curation-off/agents/refactor.md +6 -6
  96. package/dist/curation-off/commands/bug-analysis.md +69 -119
  97. package/dist/curation-off/commands/dynamic-build.md +23 -23
  98. package/dist/curation-off/commands/implement.md +46 -32
  99. package/dist/curation-off/commands/release.md +1 -1
  100. package/dist/curation-off/commands/resolve.md +35 -31
  101. package/dist/curation-off/commands/self-review.md +8 -7
  102. package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
  103. package/dist/targets/claude-code/behavior-install.js +23 -9
  104. package/dist/targets/claude-code/charter-render.js +374 -0
  105. package/dist/targets/claude-code/compliance-install.js +62 -17
  106. package/dist/targets/claude-code/compliance-off-files.js +249 -0
  107. package/dist/targets/claude-code/curation-install.js +632 -117
  108. package/dist/targets/claude-code/folder-swap.js +142 -0
  109. package/dist/targets/claude-code/install-backup.js +780 -0
  110. package/dist/targets/claude-code/installer.js +795 -199
  111. package/dist/targets/claude-code/language-stamp.js +6 -0
  112. package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
  113. package/dist/targets/claude-code/linked-folder.js +176 -0
  114. package/dist/targets/claude-code/reference-converge.js +131 -0
  115. package/dist/targets/claude-code/rules-folder.js +34 -0
  116. package/dist/targets/claude-code/shipped-set.js +160 -0
  117. package/dist/targets/claude-code/tracker-install.js +21 -8
  118. package/dist/targets/claude-code/tracker-servers.js +20 -6
  119. package/dist/targets/claude-code/workflow-folder-install.js +251 -0
  120. package/dist/variant-axes.json +109 -0
  121. package/package.json +1 -1
  122. package/src/assets/agents/_partials/_code.mds +6 -6
  123. package/src/assets/agents/evaluate.md +1 -1
  124. package/src/assets/agents/git.mds +32 -0
  125. package/src/assets/agents/harden.mds +7 -6
  126. package/src/assets/agents/test.md +21 -12
  127. package/src/assets/agents/triage.mds +2 -0
  128. package/src/assets/agents/validate.md +10 -10
  129. package/src/assets/commands/_partials/_compliance.mds +5 -3
  130. package/src/assets/commands/_partials/_engine.mds +9 -8
  131. package/src/assets/commands/_partials/_publication.mds +10 -1
  132. package/src/assets/commands/bug-analysis.mds +80 -126
  133. package/src/assets/commands/code-review.mds +43 -0
  134. package/src/assets/commands/dynamic-build.mds +109 -15
  135. package/src/assets/commands/dynamic-tickets.mds +13 -0
  136. package/src/assets/commands/implement.mds +145 -33
  137. package/src/assets/commands/plan.mds +19 -0
  138. package/src/assets/commands/release.mds +50 -1
  139. package/src/assets/commands/resolve.mds +217 -32
  140. package/src/assets/commands/self-review.mds +8 -7
  141. package/src/assets/mds/git/_ops.mds +23 -0
  142. package/src/assets/mds/git/_ops_steps.mds +17 -0
  143. package/src/assets/mds/git/_pr.mds +1 -1
  144. package/src/assets/mds/git/_references.mds +6 -0
  145. package/src/assets/mds/tracker/_common.mds +4 -0
  146. package/src/assets/mds/tracker/_github.mds +42 -0
  147. package/src/assets/mds/tracker/_jira.mds +43 -0
  148. package/src/assets/mds/tracker/_linear.mds +43 -0
  149. package/src/assets/rules/quality.md +2 -0
  150. package/src/assets/scripts/ci-wait.cjs +204 -126
  151. package/src/assets/scripts/hooks/assets/implement-installed +1 -0
  152. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
  153. package/src/assets/scripts/hooks/preamble +18 -8
  154. package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
  155. package/src/assets/scripts/pr-evidence.cjs +41 -2
  156. package/src/assets/scripts/verify-evidence.cjs +408 -56
  157. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
  158. package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
  159. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
  160. package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -2
  162. package/src/assets/skills/git/SKILL.md +3 -3
  163. package/src/assets/skills/git/references/github-api.md +2 -139
  164. package/src/assets/skills/git/references/github-posting.md +158 -0
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +1 -1
  167. package/src/assets/skills/review-methodology/references/report-template.md +1 -2
  168. package/src/assets/skills/testing/SKILL.md +2 -4
@@ -5,9 +5,11 @@
5
5
  // /resolve Phase 8 call it inline, through the Bash tool, instead of re-spawning the
6
6
  // Git agent once a minute: a poll costs no model tokens, so the wait happens here.
7
7
  // It reads CI with `gh pr view` and `gh pr checks`, plus a read-only `gh api` probe of
8
- // the repository's workflow list when the check list stays empty (D-CI-WORKFLOWS): at
9
- // most one answered probe per run; a failed probe is a failed poll, and the next poll
10
- // probes again. It classifies the head it was told about, and prints exactly one line.
8
+ // the repository's workflow list when the check list is empty or holds external checks
9
+ // only (D-CI-WORKFLOWS): at most one answered probe per run, and its answer is kept for
10
+ // the run; a failed probe is a failed poll, and the next poll probes again. It waits for
11
+ // CI to start until CI_WAIT_MAX_SECONDS, classifies the head it was told about, and
12
+ // prints exactly one line.
11
13
  // Installed beside hud.sh and redact-secrets.cjs under ~/.devflow/scripts/.
12
14
  //
13
15
  // Usage:
@@ -27,7 +29,8 @@
27
29
  // unparseable line, an unlisted status or a non-zero exit as INDETERMINATE.
28
30
  //
29
31
  // Design constraints (binding). The text of each verdict rule is written once, at the
30
- // marker named below; this list points to it.
32
+ // marker named below; this list points to it. The workflow-probe rule has two readers,
33
+ // mergeGate and matchedStep, and one reading of the probe value, probeAnswer.
31
34
  // - D-CI-WAIT-INLINE: this is plumbing. The only judgment in it is the classifier
32
35
  // (D-CI-CLASSIFIER, D-CI-WORKFLOW-RULE) and the merge gate (D-CI-MERGE-STATE,
33
36
  // D-CI-WORKFLOWS, D-CI-PR-STATE). Every subprocess is `gh`, spawned through exec()
@@ -35,9 +38,10 @@
35
38
  // gh output never reaches stdout raw: a check name passes sanitizeName() first.
36
39
  // - D-CI-WORKFLOW-RULE: see classifyChecks. PASSING does not prove that the test
37
40
  // workflow ran by name.
38
- // - D-CI-MERGE-STATE, D-CI-WORKFLOWS: see mergeGate. The workflow files are listed,
39
- // never opened: no workflow YAML is parsed, so which events a workflow reacts to is
40
- // not judged.
41
+ // - D-CI-MERGE-STATE, D-CI-WORKFLOWS: see mergeGate (an empty check list) and matchedStep
42
+ // (rows that are all external checks, which read the probe answer themselves). The
43
+ // workflow files are listed, never opened: no workflow YAML is parsed, so which events
44
+ // a workflow reacts to is not judged.
41
45
  // - D-CI-PR-STATE, D-CI-HEAD-BINDING: see waitForCi.
42
46
  // - D-CI-NO-LITERAL-SLEEP: the wait happens inside node, through clock.sleep(),
43
47
  // never through a shell `sleep`. Claude Code 2.1.294 blocks any Bash command
@@ -114,11 +118,6 @@ const BUCKETS = Object.freeze(['pass', 'fail', 'pending', 'skipping', 'cancel'])
114
118
  * Bash timeout of 600000 ms the callers pass
115
119
  * POLL_INTERVAL_SECONDS the sleep between polls; polls cost no model tokens,
116
120
  * so the cadence is set by gh latency and rate limits
117
- * SETTLE_WINDOW_SECONDS how long a head mismatch, an empty check list or a
118
- * list of external checks only may persist before it
119
- * is a verdict (GitHub creates checks and moves the PR
120
- * head a few seconds after a push); the workflow probe
121
- * is no window of its own
122
121
  * WORKFLOWS_PAGE_ROWS the rows asked of the workflow list in its one page
123
122
  * GH_CALL_CAP_SECONDS one gh call's timeout is min(this, time remaining),
124
123
  * so the last call ends before the deadline
@@ -127,7 +126,6 @@ const BUCKETS = Object.freeze(['pass', 'fail', 'pending', 'skipping', 'cancel'])
127
126
  */
128
127
  const CI_WAIT_MAX_SECONDS = 570;
129
128
  const POLL_INTERVAL_SECONDS = 20;
130
- const SETTLE_WINDOW_SECONDS = 90;
131
129
  const GH_CALL_CAP_SECONDS = 30;
132
130
  const MAX_POLLS = 40;
133
131
  const MAX_CONSECUTIVE_GH_FAILURES = 3;
@@ -207,12 +205,27 @@ const VIEW_FIELDS = 'headRefOid,mergeable,state';
207
205
  * waited: number, reason: string | null }} CiResult
208
206
  * `head` is the first 7 hex characters.
209
207
  * @typedef {{ pr: string, head: string, exec: ExecFn, clock: Clock, env?: NodeJS.ProcessEnv }} WaitInput
210
- * @typedef {{ state: 'deadline' } | { state: 'failed' } | { state: 'pending', pending: string[] } | { state: 'open' }
208
+ * @typedef {{ state: 'deadline', rowsRead: boolean } | { state: 'failed' } | { state: 'pending', pending: string[] }
209
+ * | { state: 'open' }
210
+ * | { state: 'stall', status: string, reason: string | null }
211
211
  * | { state: 'done', result: CiResult }} PollStep
212
+ * A `stall` keeps the wait going and names the line to give if the deadline comes first. A
213
+ * `deadline` is a poll that the deadline cut short, so it is no counted poll; `rowsRead` is
214
+ * true when the poll had read check rows, or a reply it could not read, before the cut.
215
+ * @typedef {{ status: string, reason: string | null, pending: string[] }} Last
216
+ * The line the last counted poll gives at the deadline.
212
217
  * @typedef {{ exec?: ExecFn, clock?: Clock, env?: NodeJS.ProcessEnv }} MainDeps
213
218
  * @typedef {{ code: number, stdout: string }} Outcome
214
219
  */
215
220
 
221
+ /**
222
+ * The line of a wait that has no verdict at the deadline: the start of the loop, an open or a
223
+ * failed poll, and a loop that ran out of MAX_POLLS. It is shared, so it is frozen.
224
+ *
225
+ * @type {Readonly<Last>}
226
+ */
227
+ const DEADLINE_LAST = Object.freeze({ status: 'INDETERMINATE', reason: 'deadline', pending: Object.freeze([]) });
228
+
216
229
  // ---------------------------------------------------------------------------
217
230
  // Argument gate
218
231
  // ---------------------------------------------------------------------------
@@ -286,7 +299,7 @@ function formatNames(names) {
286
299
  * every multiset of 1 to 3 checks through this and through the prose clauses and
287
300
  * requires the statuses to agree, or this to be stricter only by no-workflow-check.
288
301
  * (The empty set is not classified here: an empty list is "no checks", which
289
- * waitForCi settles before it classifies.)
302
+ * waitForCi reads as no classification at all.)
290
303
  *
291
304
  * @param {readonly Check[]} checks
292
305
  * @returns {Classification}
@@ -401,6 +414,17 @@ function normalize(res) {
401
414
  };
402
415
  }
403
416
 
417
+ /**
418
+ * Whether a gh call ran out its timeout: spawnSync then reports the error code ETIMEDOUT.
419
+ * A call that failed in any other way, fast or slow, did not.
420
+ *
421
+ * @param {ExecResult | null} res
422
+ * @returns {boolean}
423
+ */
424
+ function timedOut(res) {
425
+ return res !== null && typeof res === 'object' && Boolean(res.error) && res.error.code === 'ETIMEDOUT';
426
+ }
427
+
404
428
  // ---------------------------------------------------------------------------
405
429
  // Reply parsers
406
430
  // ---------------------------------------------------------------------------
@@ -483,22 +507,6 @@ function parseActiveWorkflows(stdout) {
483
507
  // The wait
484
508
  // ---------------------------------------------------------------------------
485
509
 
486
- /**
487
- * One settle window, as a pure step. `firstAt` is when the condition was first seen,
488
- * or null when the poll before did not see it; the window opens at `now` then. It is
489
- * done once `settleMs` have passed since it opened. The caller keeps one clock per
490
- * window and stores the returned `firstAt`.
491
- *
492
- * @param {number | null} firstAt
493
- * @param {number} now
494
- * @param {number} settleMs
495
- * @returns {{ firstAt: number, done: boolean }}
496
- */
497
- function settle(firstAt, now, settleMs) {
498
- const opened = firstAt === null ? now : firstAt;
499
- return { firstAt: opened, done: now - opened >= settleMs };
500
- }
501
-
502
510
  /**
503
511
  * D-CI-MERGE-STATE: whether GitHub reports the PR as conflicting. It is the one rule for a
504
512
  * conflict. mergeGate answers 'conflict' by it, and a caller that has read only the merge
@@ -511,6 +519,19 @@ function isConflicting(mergeable) {
511
519
  return mergeable === 'CONFLICTING';
512
520
  }
513
521
 
522
+ /**
523
+ * D-CI-WORKFLOWS: what the workflow probe has said, as one answer. It is the one reading
524
+ * of the probe's value, used by mergeGate and by matchedStep alike. Only the booleans
525
+ * true and false are answers; null, left out and any other value ask for the probe.
526
+ *
527
+ * @param {unknown} workflowsActive
528
+ * @returns {'active' | 'none' | 'ask'}
529
+ */
530
+ function probeAnswer(workflowsActive) {
531
+ if (workflowsActive === true) return 'active';
532
+ return workflowsActive === false ? 'none' : 'ask';
533
+ }
534
+
514
535
  /**
515
536
  * D-CI-MERGE-STATE, D-CI-WORKFLOWS: the one merge rule. GitHub computes `mergeable`
516
537
  * lazily, and runs no pull request workflow on a conflicting PR, so the checks of a head
@@ -520,7 +541,9 @@ function isConflicting(mergeable) {
520
541
  * is read; `mergeable` is the `gh pr view` value in any shape; `workflowsActive` is what
521
542
  * the workflow probe found: true (an active workflow file), false (none), or null or
522
543
  * left out when it was not made or gave no answer. Each case has its own answer, so a
523
- * caller never reads `mergeable` or `workflowsActive` again:
544
+ * caller never reads `mergeable` again. The probe value has two readers, both through
545
+ * probeAnswer: mergeGate, for an empty list, and matchedStep, for rows that are all external
546
+ * checks:
524
547
  * 'conflict' isConflicting(mergeable), whatever the status.
525
548
  * 'await-merge-state' PASSING or NO_CI while mergeable is anything but MERGEABLE
526
549
  * (UNKNOWN, absent or odd). It is neither a verdict nor a conflict:
@@ -531,8 +554,10 @@ function isConflicting(mergeable) {
531
554
  * 'workflow-active' NO_CI on MERGEABLE, and the probe found an active workflow file:
532
555
  * a run may still start.
533
556
  * 'keep' the status stands: PASSING on MERGEABLE, NO_CI on MERGEABLE with a
534
- * probe that found no workflow file, and FAILING, PENDING and
535
- * INDETERMINATE, which do not depend on the merge state or the probe.
557
+ * probe that found no workflow file (waitForCi holds that NO_CI until
558
+ * the deadline, so checks that appear later decide), and FAILING,
559
+ * PENDING and INDETERMINATE, which do not depend on the merge state
560
+ * or the probe.
536
561
  *
537
562
  * @param {string | null} status
538
563
  * @param {unknown} mergeable
@@ -545,8 +570,9 @@ function mergeGate(status, mergeable, workflowsActive) {
545
570
  if (status === 'PASSING') return mergeableNow ? 'keep' : 'await-merge-state';
546
571
  if (status !== 'NO_CI') return 'keep';
547
572
  if (!mergeableNow) return 'await-merge-state';
548
- if (workflowsActive === true) return 'workflow-active';
549
- return workflowsActive === false ? 'keep' : 'probe';
573
+ const answer = probeAnswer(workflowsActive);
574
+ if (answer === 'active') return 'workflow-active';
575
+ return answer === 'none' ? 'keep' : 'probe';
550
576
  }
551
577
 
552
578
  /**
@@ -560,46 +586,90 @@ function done(status, reason, failing = [], pending = []) {
560
586
  return { state: 'done', status, reason, failing, pending };
561
587
  }
562
588
 
589
+ /**
590
+ * A step that keeps the wait going and names the line to give if the deadline comes first.
591
+ *
592
+ * @param {string} status
593
+ * @param {string | null} reason
594
+ * @returns {{ state: 'stall', status: string, reason: string | null }}
595
+ */
596
+ function stall(status, reason) {
597
+ return { state: 'stall', status, reason };
598
+ }
599
+
563
600
  /**
564
601
  * The outcome of one poll on the named head, as a pure function of what the poll read
565
602
  * (the rules are classifyChecks's and mergeGate's). `verdict` is the classification
566
- * of the rows, or null for an empty list. `windowDone` is whether the settle window has
567
- * run out; it only matters for an empty list and for rows that are all external
568
- * (no-workflow-check), the two cases a late GitHub Actions run can still change.
569
- * `workflowsActive` is the probe's answer (see mergeGate) and matters for an empty list
570
- * only. This function never reads `mergeable` or `workflowsActive` itself: each answer of
571
- * mergeGate maps to one step. The outcome is one of:
572
- * pending keep waiting: a pending check, or an unsettled window
603
+ * of the rows, or null for an empty list. `workflowsActive` is the probe's answer (see
604
+ * mergeGate). An empty list reads it through mergeGate, and each answer of mergeGate maps
605
+ * to one step. This function reads it itself (probeAnswer) on the external-only path only:
606
+ * rows that are all external checks (no-workflow-check), where a workflow run that has not
607
+ * started yet could still change the verdict. It never reads `mergeable` itself. The
608
+ * outcome is one of:
609
+ * pending keep waiting: a pending check
573
610
  * open a pass or an empty list while the gate awaits the merge state
574
- * probe an empty list, window done, and the gate asks for the probe: the caller makes
575
- * the workflow probe, then asks again with its answer
611
+ * probe an empty list on MERGEABLE, or external-only rows, and the probe has no
612
+ * answer yet: the caller makes the workflow probe, then asks again with its
613
+ * answer
614
+ * stall keep waiting, with a line for the deadline: NO_CI for an empty list when the
615
+ * repository has no workflow file; INDETERMINATE (no-workflow-check) for an empty
616
+ * list or external-only rows when it has an active one, since a run may still
617
+ * start. A later poll that reads checks decides instead.
576
618
  * done a final status and reason, with the failing and pending names
577
619
  *
578
620
  * @param {Classification | null} verdict
579
621
  * @param {unknown} mergeable
580
- * @param {boolean} windowDone
581
622
  * @param {boolean | null} [workflowsActive]
582
623
  * @returns {{ state: 'pending', pending: string[] } | { state: 'open' } | { state: 'probe' }
624
+ * | { state: 'stall', status: 'INDETERMINATE', reason: 'no-workflow-check' }
625
+ * | { state: 'stall', status: 'NO_CI', reason: null }
583
626
  * | ReturnType<typeof done>}
584
627
  */
585
- function matchedStep(verdict, mergeable, windowDone, workflowsActive) {
628
+ function matchedStep(verdict, mergeable, workflowsActive) {
586
629
  const gate = mergeGate(verdict === null ? 'NO_CI' : verdict.status, mergeable, workflowsActive);
587
630
  if (gate === 'conflict') return done('INDETERMINATE', 'merge-conflict');
588
631
  if (verdict === null) {
589
- if (!windowDone) return { state: 'pending', pending: [] };
590
- if (gate === 'keep') return done('NO_CI', null);
591
- if (gate === 'workflow-active') return done('INDETERMINATE', 'no-workflow-check');
592
- if (gate === 'probe') return { state: 'probe' };
593
- return { state: 'open' };
632
+ if (gate === 'keep') return stall('NO_CI', null);
633
+ if (gate === 'workflow-active') return stall('INDETERMINATE', 'no-workflow-check');
634
+ return gate === 'probe' ? { state: 'probe' } : { state: 'open' };
594
635
  }
595
636
  if (verdict.reason === 'no-workflow-check') {
596
- return windowDone ? done('INDETERMINATE', 'no-workflow-check') : { state: 'pending', pending: [] };
637
+ const answer = probeAnswer(workflowsActive);
638
+ if (answer === 'ask') return { state: 'probe' };
639
+ return answer === 'none' ? done('INDETERMINATE', 'no-workflow-check') : stall('INDETERMINATE', 'no-workflow-check');
597
640
  }
598
641
  if (verdict.status === 'PENDING') return { state: 'pending', pending: verdict.pending };
599
642
  if (gate !== 'keep') return { state: 'open' };
600
643
  return done(verdict.status, verdict.reason, verdict.failing, verdict.pending);
601
644
  }
602
645
 
646
+ /**
647
+ * The line a counted poll leaves for the deadline: PENDING with its names, a stall with its
648
+ * status and reason, and INDETERMINATE (deadline) for an open or a failed poll, which
649
+ * clears an earlier stall. A step that is not a counted poll never gets here.
650
+ *
651
+ * @param {PollStep} step
652
+ * @returns {Last}
653
+ */
654
+ function lastOf(step) {
655
+ if (step.state === 'pending') return { status: 'PENDING', reason: null, pending: step.pending };
656
+ if (step.state === 'stall') return { status: step.status, reason: step.reason, pending: [] };
657
+ return DEADLINE_LAST;
658
+ }
659
+
660
+ /**
661
+ * The line at the end of the loop. NO_CI stands only when the deadline ended the loop:
662
+ * a loop that ran out of MAX_POLLS before the deadline has not waited out CI, so it reads
663
+ * INDETERMINATE (deadline).
664
+ *
665
+ * @param {Last} last
666
+ * @param {boolean} reachedDeadline
667
+ * @returns {Last}
668
+ */
669
+ function tailOf(last, reachedDeadline) {
670
+ return last.status === 'NO_CI' && !reachedDeadline ? DEADLINE_LAST : last;
671
+ }
672
+
603
673
  /**
604
674
  * D-CI-WORKFLOWS: the workflow probe, as one result. `gh` is waitForCi's bounded gh
605
675
  * call, so the probe gets its timeout and, with no time left, no call at all. The result is
@@ -625,15 +695,14 @@ async function askWorkflows(gh) {
625
695
  * D-CI-HEAD-BINDING: wait for the CI verdict of `head`, and of nothing else. `gh pr
626
696
  * checks` has no SHA field and reports the PR's current head, so each poll reads
627
697
  * `headRefOid` first. A different value means the checks on offer belong to another
628
- * head: they are not read, the merge state is not used, and the poll is PENDING until
629
- * SETTLE_WINDOW_SECONDS have passed since the first mismatch (GitHub moves the head
630
- * and creates checks a few seconds after a push), then INDETERMINATE (head-mismatch).
631
- * A matching head clears the mismatch clock. The poll reads `headRefOid` once more after
632
- * the checks call: the checks count only when it is still `head`. A head that moved
633
- * leaves the poll open (no verdict, no settle clock moved); a second read that fails or
634
- * cannot be read is a failed poll. A PASSING needs a poll that saw this head before and
635
- * after its checks and passed the rules of classifyChecks (D-CI-WORKFLOW-RULE) and
636
- * mergeGate (D-CI-MERGE-STATE).
698
+ * head: they are not read, the merge state is not used, and the poll is a stall
699
+ * (INDETERMINATE, head-mismatch) that the wait polls through until the deadline (GitHub
700
+ * moves the head and creates checks after a push, and CI can start much later). A poll
701
+ * that sees `head` again is decided by its checks. The poll reads `headRefOid` once more
702
+ * after the checks call: the checks count only when it is still `head`. A head that moved
703
+ * leaves the poll open (no verdict); a second read that fails or cannot be read is a failed
704
+ * poll. A PASSING needs a poll that saw this head before and after its checks and passed
705
+ * the rules of classifyChecks (D-CI-WORKFLOW-RULE) and mergeGate (D-CI-MERGE-STATE).
637
706
  *
638
707
  * D-CI-PR-STATE: GitHub reports `mergeable` UNKNOWN for a merged PR for good (gh 2.88.1,
639
708
  * probed on 2026-10-22), so a PR that is no longer open would wait to the deadline for a
@@ -641,29 +710,45 @@ async function askWorkflows(gh) {
641
710
  * the exact string OPEN ends the run at once as INDETERMINATE (not-open), before the head
642
711
  * is compared, and a `state` that is not a string is unparseable. Never PASSING.
643
712
  *
644
- * D-CI-MERGE-STATE, D-CI-WORKFLOWS: the rules are mergeGate's; this loop acts on its
645
- * answers. On a matching head, `mergeable` CONFLICTING is INDETERMINATE (merge-conflict)
646
- * at once, before the checks are read. An open step (the gate awaits the merge state) is
647
- * not PENDING, so a head that never reads MERGEABLE ends INDETERMINATE (deadline). Rows
648
- * that are all external checks share the empty list's settle window, which a pending or
649
- * failing row clears, and end INDETERMINATE (no-workflow-check) when it runs out. A step
650
- * that asks for the probe gets it from askWorkflows, `gh api` over WORKFLOWS_ARGS,
651
- * through the same bounded gh call (timeout min(GH_CALL_CAP_SECONDS, time left), none
652
- * when no time is left, which ends the loop as for any call). At most one probe is
653
- * answered per run: an answered probe ends the run in that poll, so no later poll reads
654
- * it and no settle() clock runs for it. A probe whose gh call fails is a failed poll, and
655
- * the next poll probes again; a reply parseActiveWorkflows cannot read is INDETERMINATE
656
- * (unparseable). No other path makes the probe.
713
+ * D-CI-MERGE-STATE, D-CI-WORKFLOWS: the rules are mergeGate's and matchedStep's; this loop
714
+ * acts on their answers. On a matching head, `mergeable` CONFLICTING is INDETERMINATE
715
+ * (merge-conflict) at once, before the checks are read. An open step (the gate awaits the
716
+ * merge state) is not PENDING, so a head that never reads MERGEABLE ends INDETERMINATE
717
+ * (deadline). An empty list on MERGEABLE, and rows that are all external checks, need the
718
+ * workflow probe. Rows that are all external checks end INDETERMINATE (no-workflow-check) at
719
+ * once when the repository has no active workflow file. In the other cases a workflow run
720
+ * may still start, so the step is a stall that keeps the wait going: an empty list with no
721
+ * workflow file stalls as NO_CI, and an empty list or external-only rows with an active
722
+ * workflow file stall as INDETERMINATE (no-workflow-check). Checks that appear decide.
723
+ * A step that asks for the probe gets it from askWorkflows, `gh api` over WORKFLOWS_ARGS,
724
+ * through the same bounded gh call (timeout min(GH_CALL_CAP_SECONDS, time left), none when
725
+ * no time is left, which ends the loop as for any call). At most one probe is answered per
726
+ * run: the first answer is kept and goes to every later poll, so no later poll asks again.
727
+ * A probe whose gh call fails is a failed poll, and the next poll probes again; a reply
728
+ * parseActiveWorkflows cannot read is INDETERMINATE (unparseable) at once. No other path
729
+ * makes the probe.
657
730
  *
658
731
  * Per poll: `gh pr view <n> --json headRefOid,mergeable,state`, then `gh pr checks <n>
659
732
  * --json name,state,bucket,workflow` (exit 0 or 8 are results, as in check-ci-status
660
733
  * step 3), then the same view call again (a bounded gh call like the others, not made
661
- * when no time is left), then, on that one path, the workflow probe. A PENDING or open
662
- * poll sleeps min(POLL_INTERVAL_SECONDS, remaining) and polls again; any other result
663
- * returns. A poll whose gh call fails is not a result: three failed polls in a row are
664
- * INDETERMINATE (gh-failed), fewer are retried. The loop stops at the deadline or after
665
- * MAX_POLLS with its last state: PENDING stays PENDING, and anything unknown, an open
666
- * step included, becomes INDETERMINATE (deadline).
734
+ * when no time is left), then, on the paths that need it and only while the probe has no
735
+ * answer, the workflow probe. A PENDING, open or stall poll sleeps min(POLL_INTERVAL_SECONDS,
736
+ * remaining) and polls again; any other result returns. A poll whose gh call fails is not a
737
+ * result: three failed polls in a row are INDETERMINATE (gh-failed), fewer are retried; a
738
+ * pending, open or stall poll in between clears the count. A gh call that the time left cut
739
+ * below GH_CALL_CAP_SECONDS and that ran out that timeout is the deadline, not a failed poll:
740
+ * that poll does not count. Any other failure of that call is a failed poll.
741
+ *
742
+ * The loop stops at the deadline or after MAX_POLLS. The last counted poll gives the line:
743
+ * PENDING stays PENDING, a stall gives its status and reason, and an open or a failed poll,
744
+ * which clears an earlier stall, gives INDETERMINATE (deadline). NO_CI stands only when the
745
+ * deadline ended the loop; a loop that ran out of MAX_POLLS first is INDETERMINATE
746
+ * (deadline). One cut poll changes the line: when the deadline cuts the second head read or
747
+ * the workflow probe after the checks call gave rows (or a reply that cannot be read), those
748
+ * rows contradict an earlier stall, so the stall does not survive and the line is
749
+ * INDETERMINATE (deadline). An earlier PENDING stands. A cut before any row came back (the
750
+ * first head read, the checks call, or a later call after an empty list) leaves the line of
751
+ * the last counted poll.
667
752
  *
668
753
  * It never throws: an exec that throws is a gh failure.
669
754
  *
@@ -675,7 +760,6 @@ async function waitForCi(input) {
675
760
  const env = input.env || childEnv();
676
761
  const startedAt = clock.now();
677
762
  const deadline = startedAt + CI_WAIT_MAX_SECONDS * 1000;
678
- const settleMs = SETTLE_WINDOW_SECONDS * 1000;
679
763
 
680
764
  /** @returns {CiResult} */
681
765
  const finish = (status, reason, failing, pending) => ({
@@ -689,8 +773,10 @@ async function waitForCi(input) {
689
773
  });
690
774
 
691
775
  /**
692
- * One gh call, its timeout min(GH_CALL_CAP_SECONDS, time left); null when no time
693
- * is left, in which case no call is made.
776
+ * One gh call, its timeout min(GH_CALL_CAP_SECONDS, time left). It is null, with no
777
+ * call made, when no time is left. It is also null for a call that the time left held
778
+ * below GH_CALL_CAP_SECONDS and that ran out that timeout: the deadline cut it short, so
779
+ * it is no failed poll. A call that fails in any other way is a failed poll.
694
780
  *
695
781
  * @param {string[]} args
696
782
  * @returns {Promise<GhAnswer | null>}
@@ -711,11 +797,12 @@ async function waitForCi(input) {
711
797
  } catch (_) {
712
798
  res = null;
713
799
  }
714
- return normalize(res);
800
+ const cutShort = left < GH_CALL_CAP_SECONDS * 1000 && timedOut(res);
801
+ return cutShort ? null : normalize(res);
715
802
  };
716
803
 
717
- let firstMismatchAt = null;
718
- let firstEmptyAt = null;
804
+ /** The probe's first answer, kept for the run; null until a probe answers. */
805
+ let workflowsActive = null;
719
806
 
720
807
  /**
721
808
  * A step that ends the run with a result.
@@ -744,90 +831,82 @@ async function waitForCi(input) {
744
831
 
745
832
  /**
746
833
  * One poll: the view, then, on a matching head that does not conflict, the checks, then
747
- * the view again, then, on the one path that needs it, the workflow probe. Both settle
748
- * windows run here, after the second view, so a poll that does not count moves neither.
834
+ * the view again, then, on the paths that need it while the probe has no answer, the
835
+ * workflow probe.
749
836
  *
750
837
  * @returns {Promise<PollStep>}
751
838
  */
752
839
  const readPoll = async () => {
753
840
  const view = await gh(viewArgs);
754
- if (view === null) return { state: 'deadline' };
841
+ if (view === null) return { state: 'deadline', rowsRead: false };
755
842
  if (view.answered && view.status === 1 && NO_PR_RE.test(view.stderr)) return resultStep('NO_PR', null, [], []);
756
843
  if (!view.answered || view.status !== 0) return { state: 'failed' };
757
844
  const parsed = parseView(view.stdout);
758
845
  if (parsed === null) return resultStep('INDETERMINATE', 'unparseable', [], []);
759
846
  if (parsed.state !== 'OPEN') return resultStep('INDETERMINATE', 'not-open', [], []);
760
- if (parsed.oid !== head) {
761
- const mismatch = settle(firstMismatchAt, clock.now(), settleMs);
762
- firstMismatchAt = mismatch.firstAt;
763
- return mismatch.done ? resultStep('INDETERMINATE', 'head-mismatch', [], []) : { state: 'pending', pending: [] };
764
- }
765
- firstMismatchAt = null;
847
+ if (parsed.oid !== head) return stall('INDETERMINATE', 'head-mismatch');
766
848
  if (isConflicting(parsed.mergeable)) return resultStep('INDETERMINATE', 'merge-conflict', [], []);
767
849
 
768
850
  const checks = await gh(['pr', 'checks', pr, '--json', 'name,state,bucket,workflow']);
769
- if (checks === null) return { state: 'deadline' };
851
+ if (checks === null) return { state: 'deadline', rowsRead: false };
770
852
  const isResult = checks.answered && (checks.status === 0 || checks.status === 8);
771
853
  const isEmpty = checks.answered && checks.status === 1 && NO_CHECKS_RE.test(checks.stderr);
772
854
  if (!isResult && !isEmpty) return { state: 'failed' };
855
+ const rows = isResult ? parseChecks(checks.stdout) : [];
773
856
  const second = await gh(viewArgs);
774
- if (second === null) return { state: 'deadline' };
857
+ if (second === null) return { state: 'deadline', rowsRead: rows === null || rows.length > 0 };
775
858
  const after = second.answered && second.status === 0 ? parseView(second.stdout) : null;
776
859
  if (after === null) return { state: 'failed' };
777
860
  if (after.oid !== head) return { state: 'open' };
778
- const rows = isResult ? parseChecks(checks.stdout) : [];
779
861
  if (rows === null) return resultStep('INDETERMINATE', 'unparseable', [], []);
780
862
  const verdict = rows.length > 0 ? classifyChecks(rows) : null;
781
- let windowDone = false;
782
- if (verdict === null || verdict.reason === 'no-workflow-check') {
783
- const empty = settle(firstEmptyAt, clock.now(), settleMs);
784
- firstEmptyAt = empty.firstAt;
785
- windowDone = empty.done;
786
- } else {
787
- firstEmptyAt = null;
788
- }
789
863
 
790
- const next = matchedStep(verdict, parsed.mergeable, windowDone, null);
864
+ const next = matchedStep(verdict, parsed.mergeable, workflowsActive);
791
865
  if (next.state !== 'probe') return stepOf(next);
792
866
  const asked = await askWorkflows(gh);
793
- if (asked.kind === 'deadline') return { state: 'deadline' };
867
+ if (asked.kind === 'deadline') return { state: 'deadline', rowsRead: verdict !== null };
794
868
  if (asked.kind === 'failed') return { state: 'failed' };
795
869
  if (asked.kind === 'unparseable') return resultStep('INDETERMINATE', 'unparseable', [], []);
796
- return stepOf(matchedStep(verdict, parsed.mergeable, windowDone, asked.active));
870
+ workflowsActive = asked.active;
871
+ return stepOf(matchedStep(verdict, parsed.mergeable, workflowsActive));
797
872
  };
798
873
 
799
874
  let failedPolls = 0;
800
- /** @type {{ status: string, pending: string[] }} */
801
- let last = { status: 'INDETERMINATE', pending: [] };
875
+ let reachedDeadline = false;
876
+ /** @type {Readonly<Last>} */
877
+ let last = DEADLINE_LAST;
802
878
 
803
879
  for (let poll = 0; poll < MAX_POLLS; poll++) {
804
- if (clock.now() >= deadline) break;
880
+ if (clock.now() >= deadline) {
881
+ reachedDeadline = true;
882
+ break;
883
+ }
805
884
 
806
885
  const step = await readPoll();
807
- if (step.state === 'deadline') break;
886
+ if (step.state === 'deadline') {
887
+ reachedDeadline = true;
888
+ if (step.rowsRead && last.status !== 'PENDING') last = DEADLINE_LAST;
889
+ break;
890
+ }
808
891
  if (step.state === 'done') return step.result;
809
892
  if (step.state === 'failed') {
810
893
  failedPolls++;
811
894
  if (failedPolls >= MAX_CONSECUTIVE_GH_FAILURES) return finish('INDETERMINATE', 'gh-failed', [], []);
812
- last = { status: 'INDETERMINATE', pending: [] };
813
- } else if (step.state === 'pending') {
814
- failedPolls = 0;
815
- last = { status: 'PENDING', pending: step.pending };
816
895
  } else {
817
- // An open step: no verdict, and the deadline exit after it is INDETERMINATE, never
818
- // PENDING.
819
896
  failedPolls = 0;
820
- last = { status: 'INDETERMINATE', pending: [] };
821
897
  }
898
+ last = lastOf(step);
822
899
 
823
900
  const left = deadline - clock.now();
824
- if (left <= 0) break;
901
+ if (left <= 0) {
902
+ reachedDeadline = true;
903
+ break;
904
+ }
825
905
  await clock.sleep(Math.min(POLL_INTERVAL_SECONDS * 1000, left));
826
906
  }
827
907
 
828
- return last.status === 'PENDING'
829
- ? finish('PENDING', null, [], last.pending)
830
- : finish('INDETERMINATE', 'deadline', [], []);
908
+ const tail = tailOf(last, reachedDeadline);
909
+ return finish(tail.status, tail.reason, [], tail.pending);
831
910
  }
832
911
 
833
912
  // ---------------------------------------------------------------------------
@@ -924,7 +1003,6 @@ module.exports = Object.freeze({
924
1003
  BUCKETS,
925
1004
  CI_WAIT_MAX_SECONDS,
926
1005
  POLL_INTERVAL_SECONDS,
927
- SETTLE_WINDOW_SECONDS,
928
1006
  GH_CALL_CAP_SECONDS,
929
1007
  MAX_POLLS,
930
1008
  MAX_CONSECUTIVE_GH_FAILURES,
@@ -0,0 +1 @@
1
+ # devflow writes this file while devflow-implement is installed. The session preamble only checks that the file exists.
@@ -1,22 +1,24 @@
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, 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.
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) — pick the roster agent that fits the work:
6
+ Routing (Agent tool), by kind of work:
7
7
  - Search and listing: Explore. Codebase orientation: Skim.
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: "rename X to Y everywhere", moves, boilerplate; `OPERATION: edit`), Polish (judgment pass for clarity that keeps behaviour: "make this code cleaner"), Validate (build, typecheck, lint, test), Git (git/GitHub).
9
- - Bug fix: Code (`OPERATION: implement`; `issue-fix` only for Triage-classified review issues). Fix: gate failures only (`validation-fix`, `alignment-fix`, `qa-fix`, `ci-fix`).
10
- - Analysis, design, research: Design, Research, Review, Triage (validate review issues against blast-radius matrix).
11
- - Real-scale work matching a workflow: invoke the full skill instead — devflow:implement, devflow:plan, devflow:research, devflow:explore, devflow:debug (all debug work, never the Debug agent), devflow:code-review, devflow:resolve.
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
+ - 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
+ - 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>`).
12
+ - A new knowledge base, only if the user asks: Knowledge (slug, name, directories).
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).
12
14
  - Anything else: the closest roster agent, never general-purpose.
13
15
 
14
- Stays mainline (judgment work): conversation, decisions, routing, synthesizing agent reports, answers already in loaded context, one targeted Read to scope a delegation.
16
+ Stays mainline (judgment work): conversation, decisions, routing, combining a few agent reports, answers already in loaded context, one targeted Read to scope a delegation.
15
17
 
16
18
  Operating rules:
17
- - Decompose mainline. Subagents cannot spawn subagents — you own task breakdown, then delegate leaf tasks.
19
+ - Decompose mainline. Subagents cannot spawn subagents: you own task breakdown, then delegate leaf tasks.
18
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.
19
- - Report cap: ask every direct delegation for a final report of at most about 1,500 tokens — findings, paths and verdicts, not file dumps.
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.
20
22
  - Parallelize independent delegations in one message. Git operations stay sequential.
21
- - 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 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.
22
- - 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.
23
+ - 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 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
+ - 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.