@khoavu882/doflow 2.0.0 → 2.0.2
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/README.md +4 -5
- package/bin/doflow.js +1 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +21 -1
- package/core/harnesses/claude/settings/settings.json +8 -9
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
- package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
- package/core/harnesses/copilot/hooks/doflow.json +0 -6
- package/core/harnesses/kiro/agents/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
- package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
- package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
- package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
- package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
- package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +26 -2
- package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
- package/core/registry/contracts.json +1 -1
- package/core/registry/contribution-points.json +5 -2
- package/core/registry/gates.json +5 -5
- package/core/registry/harnesses.json +9 -9
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +176 -503
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/packages/doflow/doflow-package.json +2 -2
- package/core/registry/rules.json +8 -8
- package/core/registry/settings.json +12 -16
- package/core/registry/storage.json +9 -2
- package/core/registry/telemetry.json +1 -1
- package/core/registry/verification.json +0 -5
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
- package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +2 -2
- package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
- package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
- package/core/shared/scripts/doflow/bin/doflow-run +23 -2
- package/core/shared/skills/do/SKILL.md +3 -3
- package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
- package/core/shared/skills/do-code-review/SKILL.md +7 -4
- package/core/shared/skills/do-design/SKILL.md +11 -5
- package/core/shared/skills/do-design/references/data_model_example.md +167 -0
- package/core/shared/skills/do-diagnose/SKILL.md +4 -2
- package/core/shared/skills/do-document/SKILL.md +1 -0
- package/core/shared/skills/do-document/references/embedded-research.md +1 -1
- package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
- package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
- package/core/shared/skills/do-flow/SKILL.md +4 -4
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-implement/SKILL.md +9 -7
- package/core/shared/skills/do-plan/SKILL.md +9 -6
- package/core/shared/templates/doflow/data-model-template.md +136 -102
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +54 -12
- package/src/adapters/claude/index.js +23 -2
- package/src/adapters/codex/agents.js +15 -1
- package/src/adapters/codex/config.js +5 -2
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +13 -0
- package/src/adapters/copilot/index.js +11 -4
- package/src/adapters/copy-tree.js +8 -2
- package/src/adapters/created-file.js +41 -0
- package/src/adapters/mcp-entries.js +11 -1
- package/src/adapters/opencode/index.js +32 -5
- package/src/adapters/pi/index.js +15 -4
- package/src/adapters/pi/mcp.js +44 -18
- package/src/cli/commands/remove.js +3 -1
- package/src/cli/commands/status.js +1 -1
- package/src/cli/commands/update.js +7 -3
- package/src/cli/index.js +43 -19
- package/src/cli/runtime-commands.js +13 -5
- package/src/cli/shared.js +5 -3
- package/src/helper/marker-merge.js +11 -7
- package/src/install/backup-set.js +7 -3
- package/src/install/backup.js +13 -7
- package/src/install/context.js +2 -1
- package/src/install/manifest.js +35 -11
- package/src/install/paths.js +38 -0
- package/src/install/targets.js +11 -2
- package/src/lifecycle/coverage.js +1 -30
- package/src/lifecycle/hook-tier.js +2 -2
- package/src/lifecycle/index.js +80 -3
- package/src/lifecycle/retired-harnesses.js +16 -1
- package/src/lifecycle/view.js +3 -2
- package/src/registry/index.js +21 -4
- package/src/release/docs-blocks.js +37 -53
- package/src/runtime/advice/doctor.js +2 -2
- package/src/runtime/brief-tools.js +2 -1
- package/src/runtime/capability-router.js +26 -3
- package/src/runtime/context/pointer.js +84 -17
- package/src/runtime/context-pack.js +52 -9
- package/src/runtime/dispatch/integrate.js +27 -8
- package/src/runtime/dispatch/loop.js +7 -0
- package/src/runtime/dispatch/verify-run.js +9 -3
- package/src/runtime/evidence-ledger.js +18 -0
- package/src/runtime/feature-pointer.js +10 -1
- package/src/runtime/health.js +68 -5
- package/src/runtime/implementation-gate.js +14 -6
- package/src/runtime/intake/engine.js +679 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/knowledge/code-retrieval.js +11 -5
- package/src/runtime/knowledge/semble-provider.js +12 -2
- package/src/runtime/lifecycle/cli.js +76 -6
- package/src/runtime/lifecycle/followup.js +32 -6
- package/src/runtime/lifecycle/overview.js +34 -3
- package/src/runtime/lifecycle/release.js +16 -3
- package/src/runtime/lifecycle/retention.js +12 -15
- package/src/runtime/lifecycle/store-upkeep.js +13 -3
- package/src/runtime/mask.js +3 -3
- package/src/runtime/mcp-consultations.js +11 -5
- package/src/runtime/packages/points.js +10 -0
- package/src/runtime/research-request.js +3 -1
- package/src/runtime/review/command.js +3 -7
- package/src/runtime/review/start.js +24 -10
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/gates.js +6 -3
- package/src/runtime/roadmap/loop.js +127 -33
- package/src/runtime/roadmap/merge.js +17 -8
- package/src/runtime/roadmap/rows.js +15 -5
- package/src/runtime/roadmap/seed.js +153 -41
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/settings/files.js +3 -1
- package/src/runtime/settings/prepared-run.js +12 -2
- package/src/runtime/settings/registry.js +1 -1
- package/src/runtime/settings/resolve.js +13 -1
- package/src/runtime/settings/write.js +73 -2
- package/src/runtime/slots.js +1 -1
- package/src/runtime/storage/registry.js +2 -2
- package/src/runtime/storage/roots.js +4 -18
- package/src/runtime/storage/rules.js +22 -46
- package/src/runtime/storage/work-status.js +14 -4
- package/src/runtime/task-classifier.js +39 -4
- package/src/runtime/task-scope.js +17 -2
- package/src/runtime/task-state.js +13 -7
- package/src/runtime/trace/analyze.js +4 -2
- package/src/runtime/trace/indicators.js +25 -20
- package/src/runtime/trace/spans.js +4 -5
- package/src/runtime/verb-view.js +268 -0
- package/src/runtime/verification/engine.js +0 -45
- package/src/runtime/worker/command.js +4 -2
- package/src/runtime/worker/launch.js +5 -37
- package/src/runtime/worker/readiness.js +10 -6
- package/src/runtime/worker/supervisor.js +18 -6
- package/src/runtime/workflow-orchestrator.js +39 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
|
@@ -0,0 +1,268 @@
|
|
|
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 === null || typeof element !== 'object' || Array.isArray(element)
|
|
91
|
+
? element : (projectValue(element, child.node) ?? {})))
|
|
92
|
+
: undefined;
|
|
93
|
+
} else {
|
|
94
|
+
part = projectValue(value[key], child.node);
|
|
95
|
+
// An object none of whose kept paths exist is absent, not an empty shell.
|
|
96
|
+
if (part !== undefined && !child.node.all && Object.keys(part).length === 0) part = undefined;
|
|
97
|
+
}
|
|
98
|
+
if (part !== undefined) out[key] = part;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Projects a full answer onto keep paths: `$` then segments `.name` or `.name[]`; a path keeps the whole value at
|
|
105
|
+
* its last segment, `[]` maps over an array (order and length kept), an absent path is skipped, and output keys
|
|
106
|
+
* follow the full answer's key order.
|
|
107
|
+
*/
|
|
108
|
+
function projectBrief(full , keep ) {
|
|
109
|
+
return projectValue(full, keepTree(keep)) ?? {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -------------------------------------------------------------------------------------------------- cursor
|
|
113
|
+
|
|
114
|
+
const CURSOR = /^([0-9]{1,15}):([0-9]{8}T[0-9]{9}Z-[0-9a-hjkmnp-tv-z]{6})$/;
|
|
115
|
+
const DIGITS = /^[0-9]+$/;
|
|
116
|
+
|
|
117
|
+
function encodeCursor(key ) {
|
|
118
|
+
if (!Number.isSafeInteger(key.atMs) || key.atMs < 0 || String(key.atMs).length > 15) {
|
|
119
|
+
throw new ViewUsageError(`cursor time must be 0 to 15 digits of milliseconds (got ${key.atMs})`);
|
|
120
|
+
}
|
|
121
|
+
return `${key.atMs}:${key.event}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The key a cursor names, or null when the text is not a cursor of this format. */
|
|
125
|
+
function decodeCursor(text ) {
|
|
126
|
+
const match = typeof text === 'string' ? CURSOR.exec(text) : null;
|
|
127
|
+
return match ? { atMs: Number(match[1]), event: match[2] } : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True when key `a` orders after key `b`: later time, or the same time and a greater event id. */
|
|
131
|
+
function keyAfter(a , b ) {
|
|
132
|
+
return a.atMs > b.atMs || (a.atMs === b.atMs && a.event > b.event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pages an item list already in ascending key order. Items at or before the cursor are dropped, then the first
|
|
137
|
+
* `limit` are kept. `nextCursor` is the last shown item's key when items remain after it, else null.
|
|
138
|
+
*/
|
|
139
|
+
function pageItems (items , opts
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
) {
|
|
143
|
+
let limit = opts.defaultLimit;
|
|
144
|
+
if (opts.limit !== undefined) {
|
|
145
|
+
const text = String(opts.limit);
|
|
146
|
+
if (!DIGITS.test(text) || Number(text) < 1) {
|
|
147
|
+
throw new ViewUsageError(`--limit must be a whole number of 1 or more (at most ${opts.maxLimit} are returned)`);
|
|
148
|
+
}
|
|
149
|
+
limit = Math.min(opts.maxLimit, Number(text));
|
|
150
|
+
}
|
|
151
|
+
let rest = items;
|
|
152
|
+
if (opts.cursor !== undefined) {
|
|
153
|
+
const after = decodeCursor(opts.cursor);
|
|
154
|
+
if (after === null) throw new ViewUsageError('--cursor must be the nextCursor of an earlier list');
|
|
155
|
+
rest = items.filter((item) => keyAfter(opts.key(item), after));
|
|
156
|
+
}
|
|
157
|
+
const page = limit === null ? rest.slice() : rest.slice(0, limit);
|
|
158
|
+
const more = page.length < rest.length;
|
|
159
|
+
return {
|
|
160
|
+
items: page,
|
|
161
|
+
shown: page.length,
|
|
162
|
+
nextCursor: more && page.length > 0 ? encodeCursor(opts.key(page[page.length - 1])) : null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ----------------------------------------------------------------------------------------------- next lines
|
|
167
|
+
|
|
168
|
+
const SAFE_VALUE = /^[A-Za-z0-9._:@,/-]{1,200}$/;
|
|
169
|
+
const LABELS = { page: 'Next page', 'page-full': 'Full view of this page', full: 'Full view' } ;
|
|
170
|
+
const VIEW_FLAGS = ['cursor', 'limit', 'view'];
|
|
171
|
+
|
|
172
|
+
function kebab(name ) {
|
|
173
|
+
return name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `doflow-run <verb>[ --action <action>]( --<flag>[ <value>])*`, flags in the order of `carried`. A value renders
|
|
178
|
+
* only when it is `true` (bare flag) or a short safe token (or finite number); any other value renders as the
|
|
179
|
+
* placeholder `<flag>`, so free text is never carried. `false`, null and undefined are omitted.
|
|
180
|
+
*/
|
|
181
|
+
function commandLine(verb , action , carried ) {
|
|
182
|
+
const parts = ['doflow-run', verb];
|
|
183
|
+
if (action !== undefined) parts.push('--action', action);
|
|
184
|
+
for (const [name, value] of Object.entries(carried)) {
|
|
185
|
+
if (value === undefined || value === null || value === false) continue;
|
|
186
|
+
const flag = kebab(name);
|
|
187
|
+
parts.push(`--${flag}`);
|
|
188
|
+
if (value === true) continue;
|
|
189
|
+
const text = typeof value === 'number' && Number.isFinite(value) ? String(value) : value;
|
|
190
|
+
parts.push(typeof text === 'string' && SAFE_VALUE.test(text) ? text : `<${flag}>`);
|
|
191
|
+
}
|
|
192
|
+
return parts.join(' ');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The next line of one kind: `page` (the page after this one), `page-full` (this page in the full view, carrying
|
|
197
|
+
* the input cursor and `--limit <shown>`) or `full` (the verb's whole, bounded, full answer). The options echoed
|
|
198
|
+
* are those the spec's `carry` names, in that order, then `--cursor`, `--limit`, `--view` (a `page` line names
|
|
199
|
+
* `--view full` only when the caller's `view` is `full`). Returns no line when
|
|
200
|
+
* the kind has nothing to point at (`page` without a nextCursor, `page-full` with nothing shown). In a JSON answer
|
|
201
|
+
* (`json: true`) every line ends with `--json`, so the command as written prints the same JSON shape; a text
|
|
202
|
+
* answer's lines carry no `--json`.
|
|
203
|
+
*/
|
|
204
|
+
function viewLines(kind , ctx
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
) {
|
|
208
|
+
const args = {};
|
|
209
|
+
for (const name of ctx.spec.carry) {
|
|
210
|
+
if (!VIEW_FLAGS.includes(name)) args[name] = ctx.carried[name];
|
|
211
|
+
}
|
|
212
|
+
if (kind === 'page') {
|
|
213
|
+
if (!ctx.nextCursor) return [];
|
|
214
|
+
args.cursor = ctx.nextCursor;
|
|
215
|
+
args.limit = ctx.spec.carry.includes('limit') ? ctx.carried.limit : undefined;
|
|
216
|
+
// A walk stays in the view it started in: brief is the default, so only the full view is named.
|
|
217
|
+
if (ctx.view === 'full') args.view = 'full';
|
|
218
|
+
} else if (kind === 'page-full') {
|
|
219
|
+
if (!ctx.shown || ctx.shown < 1) return [];
|
|
220
|
+
args.cursor = ctx.cursor ?? undefined;
|
|
221
|
+
args.limit = ctx.shown;
|
|
222
|
+
args.view = 'full';
|
|
223
|
+
} else {
|
|
224
|
+
args.view = 'full';
|
|
225
|
+
}
|
|
226
|
+
const asJson = ctx.json === true ? ' --json' : '';
|
|
227
|
+
return [`${LABELS[kind]}: ${commandLine(ctx.spec.verb, ctx.spec.action, args)}${asJson}`];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Adds the envelope to an answer body: `view` first; `shown` and `nextCursor` directly after `count` when the body
|
|
232
|
+
* has one, else after `view`; `next` last (replacing a `next` the body already carries).
|
|
233
|
+
*/
|
|
234
|
+
function withEnvelope(body , env
|
|
235
|
+
|
|
236
|
+
) {
|
|
237
|
+
const paging = {};
|
|
238
|
+
if (env.shown !== undefined) paging.shown = env.shown;
|
|
239
|
+
if (env.nextCursor !== undefined) paging.nextCursor = env.nextCursor;
|
|
240
|
+
const out = { view: env.view };
|
|
241
|
+
const hasCount = 'count' in body;
|
|
242
|
+
if (!hasCount) Object.assign(out, paging);
|
|
243
|
+
for (const [key, value] of Object.entries(body)) {
|
|
244
|
+
if (key === 'view' || key === 'shown' || key === 'nextCursor' || key === 'next') continue;
|
|
245
|
+
out[key] = value;
|
|
246
|
+
if (key === 'count') Object.assign(out, paging);
|
|
247
|
+
}
|
|
248
|
+
const next = env.next ?? (body.next );
|
|
249
|
+
if (next !== undefined) out.next = next;
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --------------------------------------------------------------------------------------------- serialization
|
|
254
|
+
|
|
255
|
+
/** Compact JSON off a terminal, two-space indented on one; `isTTY` defaults to `process.stdout.isTTY === true`. */
|
|
256
|
+
function serializeJson(value , opts = {}) {
|
|
257
|
+
const isTTY = opts.isTTY ?? process.stdout.isTTY === true;
|
|
258
|
+
return isTTY === true ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function emitJson(value , opts = {}) {
|
|
262
|
+
console.log(serializeJson(value, opts));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
ViewUsageError, resolveView, projectBrief, pageItems, encodeCursor, decodeCursor,
|
|
267
|
+
commandLine, viewLines, withEnvelope, serializeJson, emitJson,
|
|
268
|
+
};
|
|
@@ -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.
|
|
@@ -285,7 +285,7 @@ function evaluate({ reg, worker, harness, needs, coverage, env, cwd, root, nowMs
|
|
|
285
285
|
nowMs,
|
|
286
286
|
);
|
|
287
287
|
const requirements = readiness.requirementsFor({ harness, worker }, facts, needs, coverage, edit);
|
|
288
|
-
return { requirements, version: found.version, ...readiness.eligibility(requirements) };
|
|
288
|
+
return { requirements, version: found.version, binaryPath: found.path, ...readiness.eligibility(requirements) };
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
/** IC-011 steps 0 to 12, then the supervisor spawn and the start poll. */
|
|
@@ -365,7 +365,9 @@ async function runAction(o ) {
|
|
|
365
365
|
edit: editRecord, childOf, request: childOf ? o.request : null,
|
|
366
366
|
...launchRecord(worker, identity, prompt.bytes.toString('utf8'), edit, binding),
|
|
367
367
|
...(binding ? { binding: { model: binding.model, flag: binding.flag } } : {}),
|
|
368
|
-
|
|
368
|
+
// The binary the probe found, as an absolute path: the supervisor spawns from the execution worktree,
|
|
369
|
+
// where a relative PATH entry would name another file than the one probed.
|
|
370
|
+
descriptor: evaluated.binaryPath && worker?.cli ? { ...worker, cli: { ...worker.cli, binary: evaluated.binaryPath } } : worker,
|
|
369
371
|
prompt: { path: prompt.path, sha256: prompt.sha256, bytes: prompt.bytes.length },
|
|
370
372
|
needs: evaluated.requirements.map((item ) => ({ requirement: item.requirement, at: item.at ?? null })),
|
|
371
373
|
};
|
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
77
|
-
* the
|
|
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
|
|
|
@@ -78,9 +78,13 @@ function workerDescriptors({ descriptors, registry, view } = {})
|
|
|
78
78
|
return ids.filter((id) => Object.hasOwn(descriptors, id)).map((id) => ({ harness: id, worker: descriptors[id] }));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The first executable `binary` on `env.PATH`, or null. Starts no process. A relative PATH entry is
|
|
83
|
+
* read against `cwd` (the caller's directory) when one is given, so the path found is absolute and
|
|
84
|
+
* names the same file whichever directory a later process starts in.
|
|
85
|
+
*/
|
|
86
|
+
function findOnPath(binary , env , platform = process.platform, cwd ) {
|
|
87
|
+
const dirs = String(env?.PATH ?? '').split(path.delimiter).filter(Boolean).map((dir) => (cwd ? path.resolve(cwd, dir) : dir));
|
|
84
88
|
const suffixes = platform === 'win32'
|
|
85
89
|
? ['', ...String(env?.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]
|
|
86
90
|
: [''];
|
|
@@ -116,7 +120,7 @@ function discoverOne({ harness, worker } , { env, cwd, now, platform } )
|
|
|
116
120
|
if (!supported(cli)) {
|
|
117
121
|
return { harness, binary: null, present: false, path: null, version: null, reason: 'cli-unknown', at: iso(now()) };
|
|
118
122
|
}
|
|
119
|
-
const found = findOnPath(cli.binary, env, platform);
|
|
123
|
+
const found = findOnPath(cli.binary, env, platform, cwd);
|
|
120
124
|
if (!found) return { harness, binary: cli.binary, present: false, path: null, version: null, reason: 'not-on-path', at: iso(now()) };
|
|
121
125
|
const execFileImpl = (bin , args , options ) => execFileSync(bin, args, { ...options, env, cwd });
|
|
122
126
|
const result = runProbe([found, ...cli.versionArgv], { execFileImpl, timeoutMs: PROBE_TIMEOUT_MS });
|
|
@@ -487,8 +491,8 @@ const CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
|
487
491
|
const FACT_RESULT = Object.freeze({ yes: 'met', no: 'failed', unknown: 'unknown' });
|
|
488
492
|
|
|
489
493
|
/**
|
|
490
|
-
* DEC-044: the `blocking-hook` policies, built from the coverage rows because coverage
|
|
491
|
-
*
|
|
494
|
+
* DEC-044: the `blocking-hook` policies, built from the coverage rows because coverage exports no
|
|
495
|
+
* policy list and `src/lifecycle/coverage.js` does not change (IC-032).
|
|
492
496
|
*/
|
|
493
497
|
function policyVocabulary(coverage ) {
|
|
494
498
|
const policies = new Set ((coverage?.rows || []).map((row ) => row.policy));
|
|
@@ -321,22 +321,33 @@ function boundedWait(promise , ms ) {
|
|
|
321
321
|
return Promise.race([promise, bound]).finally(() => clearTimeout(timer));
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
/**
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
/** What masking did to a retained final message, in the run record beside the stdout and stderr pointers. */
|
|
325
|
+
const ALTERED_WARNING = 'masking altered this final message: it is not suitable for byte-exact patch application; apply the worker\'s changes from its isolated worktree, not from this text';
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Reads only the bounded Codex output-last-message prefix; absent or unreadable files are optional.
|
|
329
|
+
* Returns the pointer the run record keeps (`path` relative to `stateRoot`, the masked `bytes`,
|
|
330
|
+
* whether masking `altered` the text and, when it did, the `warning`), or null when no file was read.
|
|
331
|
+
*/
|
|
332
|
+
function readLastMessage(parser , file , stateRoot ) {
|
|
333
|
+
if (typeof parser.lastMessage !== 'function') return null;
|
|
327
334
|
let fd ;
|
|
328
335
|
try {
|
|
329
336
|
fd = fs.openSync(file, 'r');
|
|
330
337
|
const bytes = Buffer.alloc(65536);
|
|
331
338
|
const length = fs.readSync(fd, bytes, 0, bytes.length, 0);
|
|
332
|
-
const
|
|
339
|
+
const raw = bytes.subarray(0, length).toString('utf8');
|
|
340
|
+
const masked = maskBody(raw).text;
|
|
333
341
|
parser.lastMessage(masked);
|
|
334
342
|
// NFR-004: the retained copy holds only the masked, bounded text, never the raw worker output.
|
|
335
343
|
fs.closeSync(fd);
|
|
336
344
|
fd = undefined;
|
|
337
|
-
try { fs.writeFileSync(file, masked, 'utf8'); } catch { fs.rmSync(file, { force: true }); }
|
|
345
|
+
try { fs.writeFileSync(file, masked, 'utf8'); } catch { fs.rmSync(file, { force: true }); return null; }
|
|
346
|
+
const altered = masked !== raw;
|
|
347
|
+
return { path: path.relative(stateRoot, file).split(path.sep).join('/'), bytes: Buffer.byteLength(masked), altered, ...(altered ? { warning: ALTERED_WARNING } : {}) };
|
|
338
348
|
} catch {
|
|
339
349
|
// Codex may not have produced this optional file; that does not change the run outcome.
|
|
350
|
+
return null;
|
|
340
351
|
} finally {
|
|
341
352
|
if (fd !== undefined) {
|
|
342
353
|
try { fs.closeSync(fd); } catch { /* unreadable/closed files are ignored */ }
|
|
@@ -509,7 +520,8 @@ async function superviseRun({
|
|
|
509
520
|
readOnly = { status: 'unverifiable', changes: [], head: { before: before.head, after: null }, refsChanged: null };
|
|
510
521
|
}
|
|
511
522
|
}
|
|
512
|
-
|
|
523
|
+
const lastMessage = lastMessageFile ? readLastMessage(parser, lastMessageFile, stateRoot) : null;
|
|
524
|
+
if (lastMessage) (output ).lastMessage = lastMessage;
|
|
513
525
|
const report = parser.end();
|
|
514
526
|
// 106 IC-002: a failure whose masked summary holds a declared quota signal is marked quota.
|
|
515
527
|
const quota = quotaOf(report, run.descriptor);
|
|
@@ -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
|
-
|
|
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
|
|
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(),
|
|
@@ -456,9 +484,13 @@ class WorkflowOrchestrator {
|
|
|
456
484
|
},
|
|
457
485
|
harnessName: displayName,
|
|
458
486
|
since,
|
|
459
|
-
addCommand: (server ) => {
|
|
487
|
+
addCommand: (server , harnessId = null) => {
|
|
460
488
|
const reach = reachOf();
|
|
461
|
-
const
|
|
489
|
+
const has = (item ) => ['selected', 'configured'].includes(item.servers[server]);
|
|
490
|
+
// The brief's own harness is where the server was missing; another harness having it says nothing.
|
|
491
|
+
const target = harnessId ? reach?.harnesses.find((item ) => item.id === harnessId) : null;
|
|
492
|
+
if (target) return has(target) ? null : reach.addCommand(target.id, target.scope, server);
|
|
493
|
+
const row = reach?.harnesses.find((item ) => !has(item));
|
|
462
494
|
return row ? reach.addCommand(row.id, row.scope, server) : null;
|
|
463
495
|
},
|
|
464
496
|
});
|
|
@@ -1465,4 +1497,4 @@ function handleOrchestrateCommand({
|
|
|
1465
1497
|
}
|
|
1466
1498
|
}
|
|
1467
1499
|
|
|
1468
|
-
module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };
|
|
1500
|
+
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"
|