devflow-kit 3.9.0 → 3.10.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 +111 -0
- package/README.md +1 -1
- package/dist/agents/code.md +6 -6
- package/dist/agents/fix.md +6 -6
- package/dist/agents/harden.md +7 -6
- package/dist/agents/refactor.md +6 -6
- package/dist/cli/commands/ambient.js +10 -2
- package/dist/cli/commands/compliance-prompts.js +9 -4
- package/dist/cli/commands/compliance.js +171 -24
- package/dist/cli/commands/curation.js +12 -7
- package/dist/cli/commands/gate-level-prompts.js +39 -25
- package/dist/cli/commands/gates.js +5 -7
- package/dist/cli/commands/init.js +142 -138
- package/dist/cli/commands/install-report.js +34 -0
- package/dist/cli/commands/legacy-sweep.js +64 -0
- package/dist/cli/commands/proxy.js +6 -1
- package/dist/cli/commands/rules.js +29 -12
- package/dist/cli/commands/skills.js +42 -9
- package/dist/cli/commands/tracker.js +9 -3
- package/dist/cli/commands/uninstall.js +277 -51
- package/dist/commands/bug-analysis.md +69 -120
- package/dist/commands/dynamic-build.md +23 -23
- package/dist/commands/implement.md +46 -32
- package/dist/commands/release.md +1 -1
- package/dist/commands/resolve.md +35 -31
- package/dist/commands/self-review.md +8 -7
- package/dist/compliance-off/agents/git.md +120 -0
- package/dist/compliance-off/agents/triage.md +180 -0
- package/dist/compliance-off/commands/bug-analysis.md +398 -0
- package/dist/compliance-off/commands/code-review.md +520 -0
- package/dist/compliance-off/commands/dynamic-build.md +1251 -0
- package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
- package/dist/compliance-off/commands/implement.md +736 -0
- package/dist/compliance-off/commands/plan.md +671 -0
- package/dist/compliance-off/commands/release.md +309 -0
- package/dist/compliance-off/commands/resolve.md +742 -0
- package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
- package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
- package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
- package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
- package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
- package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
- package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
- package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
- package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
- package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
- package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
- package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
- package/dist/core/agent-models.js +19 -4
- package/dist/core/assets.js +126 -17
- package/dist/core/compliance-off-files.js +27 -0
- package/dist/core/curation-variants.js +460 -112
- package/dist/core/flags.js +2 -2
- package/dist/core/installed-variants.js +63 -0
- package/dist/core/manifest.js +48 -5
- package/dist/core/mds-variants.js +700 -3
- package/dist/core/orphan-sweep.js +11 -2
- package/dist/core/plugins.js +65 -8
- package/dist/curation-compliance-off/agents/triage.md +176 -0
- package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
- package/dist/curation-compliance-off/commands/code-review.md +481 -0
- package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
- package/dist/curation-compliance-off/commands/implement.md +689 -0
- package/dist/curation-compliance-off/commands/plan.md +631 -0
- package/dist/curation-compliance-off/commands/release.md +279 -0
- package/dist/curation-compliance-off/commands/resolve.md +692 -0
- package/dist/curation-off/agents/code.md +6 -6
- package/dist/curation-off/agents/fix.md +6 -6
- package/dist/curation-off/agents/harden.md +7 -6
- package/dist/curation-off/agents/refactor.md +6 -6
- package/dist/curation-off/commands/bug-analysis.md +69 -119
- package/dist/curation-off/commands/dynamic-build.md +23 -23
- package/dist/curation-off/commands/implement.md +46 -32
- package/dist/curation-off/commands/release.md +1 -1
- package/dist/curation-off/commands/resolve.md +35 -31
- package/dist/curation-off/commands/self-review.md +8 -7
- package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
- package/dist/targets/claude-code/behavior-install.js +23 -9
- package/dist/targets/claude-code/charter-render.js +374 -0
- package/dist/targets/claude-code/compliance-install.js +32 -12
- package/dist/targets/claude-code/compliance-off-files.js +245 -0
- package/dist/targets/claude-code/curation-install.js +570 -100
- package/dist/targets/claude-code/installer.js +467 -62
- package/dist/targets/claude-code/language-stamp.js +6 -0
- package/dist/targets/claude-code/linked-folder.js +182 -0
- package/dist/targets/claude-code/reference-converge.js +118 -0
- package/dist/targets/claude-code/tracker-install.js +21 -8
- package/dist/targets/claude-code/tracker-servers.js +20 -6
- package/dist/targets/claude-code/workflow-folder-install.js +231 -0
- package/dist/variant-axes.json +109 -0
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +6 -6
- package/src/assets/agents/evaluate.md +1 -1
- package/src/assets/agents/git.mds +32 -0
- package/src/assets/agents/harden.mds +7 -6
- package/src/assets/agents/test.md +21 -12
- package/src/assets/agents/triage.mds +2 -0
- package/src/assets/agents/validate.md +10 -10
- package/src/assets/commands/_partials/_compliance.mds +5 -3
- package/src/assets/commands/_partials/_engine.mds +9 -8
- package/src/assets/commands/_partials/_publication.mds +10 -1
- package/src/assets/commands/bug-analysis.mds +80 -126
- package/src/assets/commands/code-review.mds +43 -0
- package/src/assets/commands/dynamic-build.mds +109 -15
- package/src/assets/commands/dynamic-tickets.mds +13 -0
- package/src/assets/commands/implement.mds +145 -33
- package/src/assets/commands/plan.mds +19 -0
- package/src/assets/commands/release.mds +50 -1
- package/src/assets/commands/resolve.mds +217 -32
- package/src/assets/commands/self-review.mds +8 -7
- package/src/assets/mds/git/_ops.mds +23 -0
- package/src/assets/mds/git/_ops_steps.mds +17 -0
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/mds/git/_references.mds +6 -0
- package/src/assets/mds/tracker/_common.mds +4 -0
- package/src/assets/mds/tracker/_github.mds +42 -0
- package/src/assets/mds/tracker/_jira.mds +43 -0
- package/src/assets/mds/tracker/_linear.mds +43 -0
- package/src/assets/rules/quality.md +2 -0
- package/src/assets/scripts/ci-wait.cjs +204 -126
- package/src/assets/scripts/hooks/assets/implement-installed +1 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
- package/src/assets/scripts/hooks/preamble +18 -8
- package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
- package/src/assets/scripts/pr-evidence.cjs +41 -2
- package/src/assets/scripts/verify-evidence.cjs +408 -56
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +7 -7
- package/src/assets/skills/docs-framework/SKILL.md +5 -2
- package/src/assets/skills/git/SKILL.md +3 -3
- package/src/assets/skills/git/references/github-api.md +2 -139
- package/src/assets/skills/git/references/github-posting.md +158 -0
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +1 -1
- package/src/assets/skills/review-methodology/references/report-template.md +1 -2
- 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
|
|
9
|
-
// most one answered probe per run
|
|
10
|
-
//
|
|
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
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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[] }
|
|
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
|
|
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`
|
|
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
|
|
535
|
-
*
|
|
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
|
-
|
|
549
|
-
|
|
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. `
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
* pending keep waiting: a pending check
|
|
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
|
|
575
|
-
* the workflow probe, then asks again with its
|
|
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,
|
|
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 (
|
|
590
|
-
if (gate === '
|
|
591
|
-
|
|
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
|
-
|
|
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
|
|
629
|
-
*
|
|
630
|
-
* and creates checks
|
|
631
|
-
*
|
|
632
|
-
* the checks call: the checks count only when it is still `head`. A head that moved
|
|
633
|
-
* leaves the poll open (no verdict
|
|
634
|
-
*
|
|
635
|
-
*
|
|
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
|
|
645
|
-
* answers. On a matching head, `mergeable` CONFLICTING is INDETERMINATE
|
|
646
|
-
* at once, before the checks are read. An open step (the gate awaits the
|
|
647
|
-
* not PENDING, so a head that never reads MERGEABLE ends INDETERMINATE
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
* the
|
|
656
|
-
*
|
|
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
|
|
662
|
-
*
|
|
663
|
-
* returns. A poll whose gh call fails is not a
|
|
664
|
-
* INDETERMINATE (gh-failed), fewer are retried
|
|
665
|
-
*
|
|
666
|
-
*
|
|
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)
|
|
693
|
-
* is left
|
|
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
|
-
|
|
800
|
+
const cutShort = left < GH_CALL_CAP_SECONDS * 1000 && timedOut(res);
|
|
801
|
+
return cutShort ? null : normalize(res);
|
|
715
802
|
};
|
|
716
803
|
|
|
717
|
-
|
|
718
|
-
let
|
|
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
|
|
748
|
-
*
|
|
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,
|
|
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
|
-
|
|
870
|
+
workflowsActive = asked.active;
|
|
871
|
+
return stepOf(matchedStep(verdict, parsed.mergeable, workflowsActive));
|
|
797
872
|
};
|
|
798
873
|
|
|
799
874
|
let failedPolls = 0;
|
|
800
|
-
|
|
801
|
-
|
|
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)
|
|
880
|
+
if (clock.now() >= deadline) {
|
|
881
|
+
reachedDeadline = true;
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
805
884
|
|
|
806
885
|
const step = await readPoll();
|
|
807
|
-
if (step.state === 'deadline')
|
|
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)
|
|
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
|
-
|
|
829
|
-
|
|
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
|
|
4
|
+
Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it, with one bounded exception: the main thread may run a single git, gh or script command whose output stays under about 40 lines, never a diff, log or test run.
|
|
5
5
|
|
|
6
|
-
Routing (Agent tool)
|
|
6
|
+
Routing (Agent tool), 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
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|