@khoavu882/doflow 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +4 -5
  2. package/bin/doflow.js +1 -1
  3. package/core/.claude-plugin/plugin.json +1 -1
  4. package/core/.codex-plugin/plugin.json +1 -1
  5. package/core/.plugin/plugin.json +1 -1
  6. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
  8. package/core/harnesses/claude/settings/settings.json +8 -9
  9. package/core/harnesses/codex/hooks/hooks.json +0 -10
  10. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
  11. package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
  12. package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
  13. package/core/harnesses/copilot/hooks/doflow.json +0 -6
  14. package/core/harnesses/kiro/agents/doflow.json +0 -10
  15. package/core/harnesses/kiro/hooks/doflow.json +0 -10
  16. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
  17. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
  18. package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
  19. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
  20. package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
  21. package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
  22. package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
  23. package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
  24. package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
  25. package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
  26. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
  27. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
  28. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
  29. package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
  30. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +1 -1
  31. package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
  32. package/core/registry/contracts.json +1 -1
  33. package/core/registry/contribution-points.json +5 -2
  34. package/core/registry/gates.json +5 -5
  35. package/core/registry/harnesses.json +9 -9
  36. package/core/registry/intake.json +53 -0
  37. package/core/registry/lifecycle.json +133 -711
  38. package/core/registry/package-templates/intake-category/files/category.json +8 -0
  39. package/core/registry/package-templates/intake-category/template.json +4 -0
  40. package/core/registry/packages/doflow/doflow-package.json +2 -2
  41. package/core/registry/rules.json +8 -8
  42. package/core/registry/settings.json +0 -4
  43. package/core/registry/storage.json +8 -0
  44. package/core/registry/telemetry.json +1 -1
  45. package/core/registry/verification.json +0 -5
  46. package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
  47. package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
  48. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
  49. package/core/shared/guidance/rules/RULES.md +2 -2
  50. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
  51. package/core/shared/scripts/doflow/bin/doflow-run +5 -2
  52. package/core/shared/skills/do/SKILL.md +3 -3
  53. package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
  54. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
  55. package/core/shared/skills/do-code-review/SKILL.md +7 -4
  56. package/core/shared/skills/do-design/SKILL.md +5 -3
  57. package/core/shared/skills/do-diagnose/SKILL.md +4 -2
  58. package/core/shared/skills/do-document/SKILL.md +1 -0
  59. package/core/shared/skills/do-document/references/embedded-research.md +1 -1
  60. package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
  61. package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
  62. package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
  63. package/core/shared/skills/do-flow/SKILL.md +4 -4
  64. package/core/shared/skills/do-flow/references/task_classes.md +5 -5
  65. package/core/shared/skills/do-implement/SKILL.md +9 -7
  66. package/core/shared/skills/do-plan/SKILL.md +9 -6
  67. package/core/shared/workflows/do-flow-chain.md +2 -0
  68. package/package.json +1 -1
  69. package/src/adapters/claude/index.js +23 -2
  70. package/src/adapters/codex/hooks.js +39 -3
  71. package/src/adapters/codex/index.js +13 -0
  72. package/src/adapters/copy-tree.js +1 -2
  73. package/src/cli/commands/status.js +1 -1
  74. package/src/cli/index.js +20 -8
  75. package/src/cli/runtime-commands.js +12 -4
  76. package/src/lifecycle/coverage.js +1 -30
  77. package/src/lifecycle/hook-tier.js +2 -2
  78. package/src/release/docs-blocks.js +32 -52
  79. package/src/runtime/context/pointer.js +67 -12
  80. package/src/runtime/context-pack.js +4 -1
  81. package/src/runtime/evidence-ledger.js +18 -0
  82. package/src/runtime/health.js +59 -1
  83. package/src/runtime/implementation-gate.js +4 -4
  84. package/src/runtime/intake/engine.js +677 -0
  85. package/src/runtime/intake/index.js +136 -0
  86. package/src/runtime/intake/manifest.js +140 -0
  87. package/src/runtime/intake/taxonomy.js +206 -0
  88. package/src/runtime/lifecycle/cli.js +73 -5
  89. package/src/runtime/lifecycle/followup.js +32 -6
  90. package/src/runtime/lifecycle/overview.js +2 -1
  91. package/src/runtime/packages/points.js +10 -0
  92. package/src/runtime/roadmap/attendance.js +137 -0
  93. package/src/runtime/roadmap/gates.js +6 -3
  94. package/src/runtime/roadmap/loop.js +113 -30
  95. package/src/runtime/roadmap/merge.js +17 -8
  96. package/src/runtime/roadmap/rows.js +13 -4
  97. package/src/runtime/roadmap/seed.js +153 -41
  98. package/src/runtime/roadmap/worktree-cleanup.js +147 -0
  99. package/src/runtime/settings/registry.js +1 -1
  100. package/src/runtime/slots.js +1 -1
  101. package/src/runtime/task-classifier.js +39 -4
  102. package/src/runtime/task-state.js +2 -3
  103. package/src/runtime/verb-view.js +267 -0
  104. package/src/runtime/verification/engine.js +0 -45
  105. package/src/runtime/worker/launch.js +5 -37
  106. package/src/runtime/worker/readiness.js +2 -2
  107. package/src/runtime/workflow-orchestrator.js +33 -5
  108. package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
  109. package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
  110. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
  111. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
  112. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
  113. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * The shared view helper for verbs that answer in a brief or a full view (feature 137, IC-002, IC-003, IC-007,
7
+ * IC-008). A verb declares its own brief spec beside its handler; this module resolves the view a call asked for,
8
+ * projects a full answer onto a keep-path list, pages an ordered item list by cursor, builds next lines from
9
+ * validated parsed options, and serializes JSON compact off a terminal and indented on one.
10
+ *
11
+ * Like `cli-result.ts`, this module requires nothing from src/runtime/ at load time, so any handler can require
12
+ * it without a cycle. It requires nothing at all.
13
+ */
14
+
15
+
16
+
17
+ /** One verb's brief view, declared beside its handler. */
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ /** The ordering key of a pageable item: the creating event's time in milliseconds, then its event id. */
26
+
27
+
28
+
29
+
30
+
31
+ /** A caller mistake in a view option: the handler reports it as a usage error (exit 2). */
32
+ class ViewUsageError extends Error {}
33
+
34
+ const VIEWS = ['brief', 'full'];
35
+
36
+ function resolveView(raw , defaultView ) {
37
+ if (raw === undefined || raw === null) return defaultView;
38
+ if (typeof raw === 'string' && VIEWS.includes(raw)) return raw ;
39
+ throw new ViewUsageError(`--view must be brief or full (got '${String(raw)}')`);
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------------------------- keep paths
43
+
44
+
45
+
46
+
47
+
48
+
49
+ const KEEP_PATH = /^\$(?:\.[A-Za-z_][A-Za-z0-9_]*(?:\[\])?)+$/;
50
+ const KEEP_SEGMENT = /\.([A-Za-z_][A-Za-z0-9_]*)(\[\])?/g;
51
+ const keepTrees = new Map ();
52
+
53
+ /** Builds the tree of a keep list; a malformed path, or one name used both as an array and not, throws. */
54
+ function keepTree(keep ) {
55
+ const cacheKey = keep.join('\n');
56
+ const cached = keepTrees.get(cacheKey);
57
+ if (cached) return cached;
58
+ const root = { all: false, children: new Map() };
59
+ for (const path of keep) {
60
+ if (!KEEP_PATH.test(path)) throw new Error(`malformed keep path '${path}'`);
61
+ let node = root;
62
+ for (const match of path.matchAll(KEEP_SEGMENT)) {
63
+ const [, name, brackets] = match;
64
+ const array = brackets !== undefined;
65
+ let child = node.children.get(name);
66
+ if (!child) {
67
+ child = { array, node: { all: false, children: new Map() } };
68
+ node.children.set(name, child);
69
+ } else if (child.array !== array) {
70
+ throw new Error(`keep path '${path}' uses '${name}' both as an array and as a single value`);
71
+ }
72
+ node = child.node;
73
+ }
74
+ node.all = true;
75
+ }
76
+ keepTrees.set(cacheKey, root);
77
+ return root;
78
+ }
79
+
80
+ function projectValue(value , node ) {
81
+ if (node.all) return value;
82
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
83
+ const out = {};
84
+ for (const key of Object.keys(value)) {
85
+ const child = node.children.get(key);
86
+ if (!child || value[key] === undefined) continue;
87
+ let part;
88
+ if (child.array) {
89
+ part = Array.isArray(value[key])
90
+ ? value[key].map((element ) => (child.node.all ? element : (projectValue(element, child.node) ?? {})))
91
+ : undefined;
92
+ } else {
93
+ part = projectValue(value[key], child.node);
94
+ // An object none of whose kept paths exist is absent, not an empty shell.
95
+ if (part !== undefined && !child.node.all && Object.keys(part).length === 0) part = undefined;
96
+ }
97
+ if (part !== undefined) out[key] = part;
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * Projects a full answer onto keep paths: `$` then segments `.name` or `.name[]`; a path keeps the whole value at
104
+ * its last segment, `[]` maps over an array (order and length kept), an absent path is skipped, and output keys
105
+ * follow the full answer's key order.
106
+ */
107
+ function projectBrief(full , keep ) {
108
+ return projectValue(full, keepTree(keep)) ?? {};
109
+ }
110
+
111
+ // -------------------------------------------------------------------------------------------------- cursor
112
+
113
+ const CURSOR = /^([0-9]{1,15}):([0-9]{8}T[0-9]{9}Z-[0-9a-hjkmnp-tv-z]{6})$/;
114
+ const DIGITS = /^[0-9]+$/;
115
+
116
+ function encodeCursor(key ) {
117
+ if (!Number.isSafeInteger(key.atMs) || key.atMs < 0 || String(key.atMs).length > 15) {
118
+ throw new TypeError(`cursor time must be 0 to 15 digits of milliseconds (got ${key.atMs})`);
119
+ }
120
+ return `${key.atMs}:${key.event}`;
121
+ }
122
+
123
+ /** The key a cursor names, or null when the text is not a cursor of this format. */
124
+ function decodeCursor(text ) {
125
+ const match = typeof text === 'string' ? CURSOR.exec(text) : null;
126
+ return match ? { atMs: Number(match[1]), event: match[2] } : null;
127
+ }
128
+
129
+ /** True when key `a` orders after key `b`: later time, or the same time and a greater event id. */
130
+ function keyAfter(a , b ) {
131
+ return a.atMs > b.atMs || (a.atMs === b.atMs && a.event > b.event);
132
+ }
133
+
134
+ /**
135
+ * Pages an item list already in ascending key order. Items at or before the cursor are dropped, then the first
136
+ * `limit` are kept. `nextCursor` is the last shown item's key when items remain after it, else null.
137
+ */
138
+ function pageItems (items , opts
139
+
140
+
141
+ ) {
142
+ let limit = opts.defaultLimit;
143
+ if (opts.limit !== undefined) {
144
+ const text = String(opts.limit);
145
+ if (!DIGITS.test(text) || Number(text) < 1) {
146
+ throw new ViewUsageError(`--limit must be a whole number of 1 or more (at most ${opts.maxLimit} are returned)`);
147
+ }
148
+ limit = Math.min(opts.maxLimit, Number(text));
149
+ }
150
+ let rest = items;
151
+ if (opts.cursor !== undefined) {
152
+ const after = decodeCursor(opts.cursor);
153
+ if (after === null) throw new ViewUsageError('--cursor must be the nextCursor of an earlier list');
154
+ rest = items.filter((item) => keyAfter(opts.key(item), after));
155
+ }
156
+ const page = limit === null ? rest.slice() : rest.slice(0, limit);
157
+ const more = page.length < rest.length;
158
+ return {
159
+ items: page,
160
+ shown: page.length,
161
+ nextCursor: more && page.length > 0 ? encodeCursor(opts.key(page[page.length - 1])) : null,
162
+ };
163
+ }
164
+
165
+ // ----------------------------------------------------------------------------------------------- next lines
166
+
167
+ const SAFE_VALUE = /^[A-Za-z0-9._:@,/-]{1,200}$/;
168
+ const LABELS = { page: 'Next page', 'page-full': 'Full view of this page', full: 'Full view' } ;
169
+ const VIEW_FLAGS = ['cursor', 'limit', 'view'];
170
+
171
+ function kebab(name ) {
172
+ return name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
173
+ }
174
+
175
+ /**
176
+ * `doflow-run <verb>[ --action <action>]( --<flag>[ <value>])*`, flags in the order of `carried`. A value renders
177
+ * only when it is `true` (bare flag) or a short safe token (or finite number); any other value renders as the
178
+ * placeholder `<flag>`, so free text is never carried. `false`, null and undefined are omitted.
179
+ */
180
+ function commandLine(verb , action , carried ) {
181
+ const parts = ['doflow-run', verb];
182
+ if (action !== undefined) parts.push('--action', action);
183
+ for (const [name, value] of Object.entries(carried)) {
184
+ if (value === undefined || value === null || value === false) continue;
185
+ const flag = kebab(name);
186
+ parts.push(`--${flag}`);
187
+ if (value === true) continue;
188
+ const text = typeof value === 'number' && Number.isFinite(value) ? String(value) : value;
189
+ parts.push(typeof text === 'string' && SAFE_VALUE.test(text) ? text : `<${flag}>`);
190
+ }
191
+ return parts.join(' ');
192
+ }
193
+
194
+ /**
195
+ * The next line of one kind: `page` (the page after this one), `page-full` (this page in the full view, carrying
196
+ * the input cursor and `--limit <shown>`) or `full` (the verb's whole, bounded, full answer). The options echoed
197
+ * are those the spec's `carry` names, in that order, then `--cursor`, `--limit`, `--view` (a `page` line names
198
+ * `--view full` only when the caller's `view` is `full`). Returns no line when
199
+ * the kind has nothing to point at (`page` without a nextCursor, `page-full` with nothing shown). In a JSON answer
200
+ * (`json: true`) the `page-full` and `full` lines end with `--json`, so the command as written prints the full
201
+ * payload; a text answer's lines carry no `--json`.
202
+ */
203
+ function viewLines(kind , ctx
204
+
205
+
206
+ ) {
207
+ const args = {};
208
+ for (const name of ctx.spec.carry) {
209
+ if (!VIEW_FLAGS.includes(name)) args[name] = ctx.carried[name];
210
+ }
211
+ if (kind === 'page') {
212
+ if (!ctx.nextCursor) return [];
213
+ args.cursor = ctx.nextCursor;
214
+ args.limit = ctx.spec.carry.includes('limit') ? ctx.carried.limit : undefined;
215
+ // A walk stays in the view it started in: brief is the default, so only the full view is named.
216
+ if (ctx.view === 'full') args.view = 'full';
217
+ } else if (kind === 'page-full') {
218
+ if (!ctx.shown || ctx.shown < 1) return [];
219
+ args.cursor = ctx.cursor ?? undefined;
220
+ args.limit = ctx.shown;
221
+ args.view = 'full';
222
+ } else {
223
+ args.view = 'full';
224
+ }
225
+ const asJson = ctx.json === true && kind !== 'page' ? ' --json' : '';
226
+ return [`${LABELS[kind]}: ${commandLine(ctx.spec.verb, ctx.spec.action, args)}${asJson}`];
227
+ }
228
+
229
+ /**
230
+ * Adds the envelope to an answer body: `view` first; `shown` and `nextCursor` directly after `count` when the body
231
+ * has one, else after `view`; `next` last (replacing a `next` the body already carries).
232
+ */
233
+ function withEnvelope(body , env
234
+
235
+ ) {
236
+ const paging = {};
237
+ if (env.shown !== undefined) paging.shown = env.shown;
238
+ if (env.nextCursor !== undefined) paging.nextCursor = env.nextCursor;
239
+ const out = { view: env.view };
240
+ const hasCount = 'count' in body;
241
+ if (!hasCount) Object.assign(out, paging);
242
+ for (const [key, value] of Object.entries(body)) {
243
+ if (key === 'view' || key === 'shown' || key === 'nextCursor' || key === 'next') continue;
244
+ out[key] = value;
245
+ if (key === 'count') Object.assign(out, paging);
246
+ }
247
+ const next = env.next ?? (body.next );
248
+ if (next !== undefined) out.next = next;
249
+ return out;
250
+ }
251
+
252
+ // --------------------------------------------------------------------------------------------- serialization
253
+
254
+ /** Compact JSON off a terminal, two-space indented on one; `isTTY` defaults to `process.stdout.isTTY === true`. */
255
+ function serializeJson(value , opts = {}) {
256
+ const isTTY = opts.isTTY ?? process.stdout.isTTY === true;
257
+ return isTTY === true ? JSON.stringify(value, null, 2) : JSON.stringify(value);
258
+ }
259
+
260
+ function emitJson(value , opts = {}) {
261
+ console.log(serializeJson(value, opts));
262
+ }
263
+
264
+ module.exports = {
265
+ ViewUsageError, resolveView, projectBrief, pageItems, encodeCursor, decodeCursor,
266
+ commandLine, viewLines, withEnvelope, serializeJson, emitJson,
267
+ };
@@ -1176,51 +1176,6 @@ class VerificationEngine {
1176
1176
  };
1177
1177
  }
1178
1178
 
1179
- /**
1180
- * Declares completion. FR-009: a task is complete only when its required checks pass, or when the
1181
- * user explicitly accepts what is still failing.
1182
- *
1183
- * @param {Object} report
1184
- * @param {Object} [acceptance]
1185
- * @param {boolean} [acceptance.userAccepted]
1186
- * @param {string} [acceptance.acceptedBy] who accepted; an unattributed acceptance is refused
1187
- * @param {string} [acceptance.note]
1188
- * @returns {{ complete: boolean, basis: string, reason: string, acceptedFailures?: Array<Object> }}
1189
- */
1190
- declareCompletion(report , acceptance = {}) {
1191
- if (!report || typeof report.status !== 'string') {
1192
- throw new Error('declareCompletion expects a report produced by runContract');
1193
- }
1194
- if (report.status === 'PASS') {
1195
- return { complete: true, basis: 'required-checks-passed', reason: report.reason };
1196
- }
1197
- if (acceptance.userAccepted !== true) {
1198
- return {
1199
- complete: false,
1200
- basis: 'not-accepted',
1201
- reason: `Verification is ${report.status}: ${report.reason} Completion needs the required checks to pass or an explicit user acceptance.`,
1202
- };
1203
- }
1204
- const requiresAccepter = (this.policy.completion || {}).overrideRequiresAccepter !== false;
1205
- if (requiresAccepter && (typeof acceptance.acceptedBy !== 'string' || acceptance.acceptedBy.trim() === '')) {
1206
- // "The user explicitly accepts" needs a user. An anonymous flag is indistinguishable from the
1207
- // caller setting it for itself.
1208
- return {
1209
- complete: false,
1210
- basis: 'acceptance-unattributed',
1211
- reason: 'Accepting a failing verification requires naming who accepted it.',
1212
- };
1213
- }
1214
- return {
1215
- complete: true,
1216
- basis: 'user-accepted-failures',
1217
- reason: acceptance.note || `Accepted by ${acceptance.acceptedBy} while ${report.status}.`,
1218
- acceptedFailures: (report.tiers || [])
1219
- .filter((t ) => t.required && t.status !== 'PASS' && t.status !== 'NOT_APPLICABLE' && t.status !== 'SUBSUMED' && t.status !== 'SKIPPED')
1220
- .map((t ) => ({ tier: t.id, status: t.status, reason: t.reason })),
1221
- };
1222
- }
1223
-
1224
1179
  /**
1225
1180
  * A stable fingerprint of what failed. Two identical fingerprints across attempts mean the retry
1226
1181
  * changed nothing observable, which FR-010 forbids as a response.
@@ -3,8 +3,7 @@
3
3
  // Feature 081 B.3 (IC-003, IC-012; DEC-030, DEC-037, DEC-045): the real launcher.
4
4
  //
5
5
  // `launchEnv` names the worker in the caller's environment: the caller's variables are inherited
6
- // unchanged (DEC-045), except for the five identity variables, DOFLOW_WORKER_RUN and the optional
7
- // project-only DOFLOW_GUARD_DROP_DATABASES launch snapshot (112). The run segment of
6
+ // unchanged (DEC-045), except for the five identity variables and DOFLOW_WORKER_RUN. The run segment of
8
7
  // the claim's run key is the worker's run, or, when that segment is the default `local`, the run
9
8
  // id without its `w-` prefix (DEC-037), so the worker always resolves to a launched identity and
10
9
  // its 078 unit `L.<run>.<lineage>.<token>` is unique to this run. A child run (082 IC-015) is
@@ -24,12 +23,9 @@
24
23
  // and the emission text is never returned for storage.
25
24
 
26
25
  const crypto = require('node:crypto');
27
- const fs = require('node:fs');
28
- const path = require('node:path');
29
26
  const { spawn } = require('node:child_process');
30
27
  const { emitContext, utf8Bytes } = require('../context/arbitrate.js');
31
28
  const { grantSummary } = require('../context/summary-store.js');
32
- const { loadSettings } = require('../settings/resolve.js');
33
29
 
34
30
 
35
31
  /** 120 IC-011 / IC-012: the binding shape consumed by the recipe assembler. */
@@ -37,11 +33,9 @@ const { loadSettings } = require('../settings/resolve.js');
37
33
 
38
34
  /** IC-012: the names a launch may set, in the order the run record lists them. The first five are the
39
35
  * identity contract; `DOFLOW_WORKER_RUN` (110) carries the worker run id `w-<12 hex>` and nothing else,
40
- * so the worker's telemetry can name its own run. The seventh variable (112) is an optional
41
- * snapshot of project-only database names. Neither is part of the stored identity. */
36
+ * so the worker's telemetry can name its own run. It is not part of the stored identity. */
42
37
  const WORKER_RUN_VAR = 'DOFLOW_WORKER_RUN';
43
- const DROP_DATABASES_VAR = 'DOFLOW_GUARD_DROP_DATABASES';
44
- const ENV_SET = Object.freeze(['DOFLOW_RUN_ID', 'DOFLOW_LINEAGE_ID', 'DOFLOW_ATTEMPT', 'DOFLOW_SESSION_ROLE', 'DOFLOW_WORKTREE', WORKER_RUN_VAR, DROP_DATABASES_VAR]);
38
+ const ENV_SET = Object.freeze(['DOFLOW_RUN_ID', 'DOFLOW_LINEAGE_ID', 'DOFLOW_ATTEMPT', 'DOFLOW_SESSION_ROLE', 'DOFLOW_WORKTREE', WORKER_RUN_VAR]);
45
39
  const PROMPT_ELEMENT = '{prompt}';
46
40
  const LAST_MESSAGE_FILE_ELEMENT = '{lastMessageFile}';
47
41
  const MODEL_ELEMENT = '{model}';
@@ -73,31 +67,8 @@ class LaunchUsageError extends Error {
73
67
  }
74
68
  }
75
69
 
76
- /** 112: read the owning project's layers at launch, never the worker checkout, caller snapshot or
77
- * the launcher's process environment. Every fault omits the exemption. A fresh env copy bypasses
78
- * settings memoisation so an earlier launch cannot pin stale authorisation. */
79
- function dropDatabases(execWorktree , env ) {
80
- try {
81
- if (!path.isAbsolute(execWorktree)) return null;
82
- const canonical = fs.realpathSync(execWorktree);
83
- if (!fs.statSync(canonical).isDirectory()) return null;
84
- const dispatch = /^(.*)\/\.doflow\/state\/dispatch-worktrees\/d-[0-9a-f]{12}$/.exec(canonical);
85
- if (!dispatch || dispatch[0] !== canonical) return null;
86
- const projectRoot = dispatch[1] || path.parse(canonical).root;
87
- const resolved = loadSettings({ projectRoot, env: { ...env } }).get('guard.workerDropDatabases', 'project');
88
- // 086's linked-checkout fallback is not authority for this exemption. An invalid project file
89
- // also voids the snapshot, even if a different layer could have supplied a usable value.
90
- if (resolved.origin.checkout !== 'own' || !['project', 'local'].includes(resolved.origin.layer)
91
- || resolved.candidates.some((c ) => ['project', 'local'].includes(c.layer) && c.status === 'invalid')) return null;
92
- const value = resolved.value;
93
- if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > 1024
94
- || /^[a-z][a-z0-9_]{0,62}( [a-z][a-z0-9_]{0,62})*$/.exec(value)?.[0] !== value || value.split(' ').length > 32) return null;
95
- return value;
96
- } catch { return null; }
97
- }
98
-
99
- /** The five identity variables, optional worker run id and project-only guard snapshot. Both
100
- * launch-only values replace (or remove) caller values; the caller's environment is never mutated. */
70
+ /** The five identity variables and the optional worker run id. The run id replaces (or removes) the
71
+ * caller's value; the caller's environment is never mutated. */
101
72
  function withIdentity(env , identity , execWorktree , runId ) {
102
73
  if (runId !== undefined && !RUN_ID.test(runId)) throw new LaunchUsageError('run-id-malformed', `launch: run id must match ${RUN_ID}: ${runId}`);
103
74
  const out = {
@@ -110,12 +81,9 @@ function withIdentity(env , identity , execWorktree ,
110
81
  };
111
82
  if (runId !== undefined) out[WORKER_RUN_VAR] = runId;
112
83
  else delete out[WORKER_RUN_VAR];
113
- delete out[DROP_DATABASES_VAR];
114
84
  // 120 IC-003: the primary declaration names the person's main session, never a worker.
115
85
  delete out.DOFLOW_PRIMARY_HARNESS;
116
86
  delete out.DOFLOW_PRIMARY_MODEL;
117
- const databases = dropDatabases(execWorktree, env);
118
- if (databases !== null) out[DROP_DATABASES_VAR] = databases;
119
87
  return out;
120
88
  }
121
89
 
@@ -487,8 +487,8 @@ const CAPABILITY_REQUIREMENTS = Object.freeze({
487
487
  const FACT_RESULT = Object.freeze({ yes: 'met', no: 'failed', unknown: 'unknown' });
488
488
 
489
489
  /**
490
- * DEC-044: the `blocking-hook` policies, built from the coverage rows because coverage's
491
- * `POLICY_ORDER` is not exported and `src/lifecycle/coverage.js` does not change (IC-032).
490
+ * DEC-044: the `blocking-hook` policies, built from the coverage rows because coverage exports no
491
+ * policy list and `src/lifecycle/coverage.js` does not change (IC-032).
492
492
  */
493
493
  function policyVocabulary(coverage ) {
494
494
  const policies = new Set ((coverage?.rows || []).map((row ) => row.policy));
@@ -196,6 +196,29 @@ function compileProgram(workflow ) {
196
196
  return nodes;
197
197
  }
198
198
 
199
+ /** The stage a loop program adds to a class that has no review stage of its own (trivial-edit). */
200
+ const LOOP_REVIEW_STAGE = Object.freeze({
201
+ id: 'review', skill: 'do-code-review', agent: 'reviewer', kind: 'review', readinessTemplate: null, optional: false,
202
+ purpose: 'Review the delivered change and report an approval status.',
203
+ });
204
+
205
+ /**
206
+ * The program the roadmap loop runs for a task class (AU-P1). The loop is unattended, so it takes
207
+ * no optional stage out of the walk: every stage is required, a class without a review stage gets
208
+ * one, and a `gate-b` follows the last review so the class's gate-b policy (core/registry/gates.json)
209
+ * can approve the merge. The feature class already has all of that. workflows.json is not changed:
210
+ * an attended `/do-implement` run keeps its gate-free program.
211
+ */
212
+ function loopWorkflow(workflow ) {
213
+ if ((workflow.gates || []).some((gate ) => gate.id === 'gate-b')) return workflow;
214
+ const stages = (workflow.stages || []).map((stage ) => ({ ...stage, optional: false }));
215
+ if (!stages.some((stage ) => stage.kind === 'review')) stages.push({ ...LOOP_REVIEW_STAGE });
216
+ const review = stages.filter((stage ) => stage.kind === 'review').at(-1);
217
+ const gate = { id: 'gate-b', name: 'Before commit or merge', afterStage: review.id, kind: 'approval', trigger: 'always',
218
+ prompt: 'Review is complete. Proceed to commit and merge?' };
219
+ return { ...workflow, stages, gates: [...(workflow.gates || []), gate] };
220
+ }
221
+
199
222
  class WorkflowOrchestrator {
200
223
 
201
224
 
@@ -285,11 +308,15 @@ class WorkflowOrchestrator {
285
308
  return run;
286
309
  }
287
310
 
288
- /** Begin a new run. An existing run for the same task id is refused — resume instead. */
289
- start({ taskId, taskClass, now } = {}) {
311
+ /** Begin a new run. An existing run for the same task id is refused — resume instead.
312
+ * `loopProgram` is the roadmap loop's option only (loopWorkflow): the run journal records it, and no
313
+ * verb passes it. */
314
+ start({ taskId, taskClass, now, loopProgram = false } = {}) {
290
315
  assertSafeId(taskId, 'taskId');
291
316
  if (this.readRun(taskId)) throw new Error(`Workflow run '${taskId}' already exists; resume it instead of restarting`);
292
- const workflow = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly
317
+ const resolved = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly
318
+ const workflow = loopProgram ? loopWorkflow(resolved) : resolved;
319
+ const looped = workflow !== resolved; // the feature class already is a loop program: its journal is unchanged
293
320
  const program = compileProgram(workflow);
294
321
  const run = {
295
322
  version: 1,
@@ -300,8 +327,9 @@ class WorkflowOrchestrator {
300
327
  state: runState('RUNNING'),
301
328
  cursor: 0,
302
329
  program,
303
- history: [{ at: iso(now), action: 'start', detail: `task-class=${taskClass}` }],
330
+ history: [{ at: iso(now), action: 'start', detail: `task-class=${taskClass}${looped ? ' program=roadmap-loop' : ''}` }],
304
331
  startedAt: iso(now),
332
+ ...(looped ? { programKind: 'roadmap-loop' } : {}),
305
333
  // The feature the run belongs to, so another checkout can tell it from a run with the same id.
306
334
  ...(this.featureSlug ? { featureSlug: this.featureSlug } : {}),
307
335
  ...this.traceFields(),
@@ -1465,4 +1493,4 @@ function handleOrchestrateCommand({
1465
1493
  }
1466
1494
  }
1467
1495
 
1468
- module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };
1496
+ module.exports = { WorkflowOrchestrator, loopWorkflow, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Claude front door: pre-bash-guard through the shared lifecycle bridge.
3
- set -uo pipefail
4
- export DOFLOW_AGENT="${DOFLOW_AGENT:-claude}"
5
- if ! . "$(dirname "$0")/../../.doflow/shared/hooks/bridge/bridge.sh" 2>/dev/null; then
6
- printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[doflow] shared hooks lack the bridge; run: doflow update -t claude"}}'
7
- exit 0
8
- fi
9
- doflow_bridge claude PreToolUse pre-bash-guard
10
- exit $?
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Codex front door: pre-bash-guard through the shared lifecycle bridge.
3
- set -uo pipefail
4
- export DOFLOW_AGENT="${DOFLOW_AGENT:-codex}"
5
- if ! . "$(dirname "$0")/../../.doflow/shared/hooks/bridge/bridge.sh" 2>/dev/null; then
6
- printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[doflow] shared hooks lack the bridge; run: doflow update -t codex"}}'
7
- exit 0
8
- fi
9
- doflow_bridge codex PreToolUse pre-bash-guard
10
- exit $?
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Copilot CLI front door: pre-bash-guard through the shared lifecycle bridge.
3
- set -uo pipefail
4
- export DOFLOW_AGENT="${DOFLOW_AGENT:-copilot}"
5
- here="$(cd "$(dirname "$0")" && pwd)"
6
- # shellcheck source=doflow-lib.sh
7
- . "$here/doflow-lib.sh"
8
- copilot_front_door "$here" PreToolUse pre-bash-guard deny
9
- exit $?
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env bash
2
- case "${OSTYPE:-}" in msys*|cygwin*|win32*) if command -v jq >/dev/null 2>&1; then jq() { command jq -b "$@"; }; fi ;; esac # native jq.exe ends lines with CRLF unless -b
3
- # Kiro front door: pre-bash-guard through the shared lifecycle bridge.
4
- set -euo pipefail
5
- export DOFLOW_AGENT="${DOFLOW_AGENT:-kiro}"
6
- here="$(dirname "$0")"
7
- # shellcheck source=lib.sh
8
- source "$here/lib.sh"
9
- raw=$(cat)
10
- payload=$(kiro_bridge_payload <<<"$raw")
11
- event="PreToolUse"
12
- if command -v jq >/dev/null 2>&1; then event=$(printf '%s' "$raw" | jq -r '.hook_event_name // "PreToolUse"' 2>/dev/null) || event="PreToolUse"; fi
13
- bridge="$here/../../.doflow/shared/hooks/bridge/bridge.sh"
14
- if [ ! -f "$bridge" ]; then
15
- printf '%s\n' '[doflow] shared hooks lack the bridge; run: doflow update -t kiro' >&2
16
- exit 2
17
- fi
18
- . "$bridge" 2>/dev/null || { printf '%s\n' '[doflow] shared hooks lack the bridge; run: doflow update -t kiro' >&2; exit 2; }
19
- if doflow_bridge kiro "$event" pre-bash-guard <<<"$payload"; then
20
- status=0
21
- else
22
- status=$?
23
- fi
24
- if [ "$status" -eq 2 ]; then printf '\n' >&2; fi
25
- exit "$status"