@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5
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/core/.antigravity-plugin/plugin.json +0 -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 +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +24 -1
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +83 -47
- package/src/adapters/claude/index.js +15 -1
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +52 -9
- package/src/adapters/copy-tree.js +4 -4
- package/src/adapters/gemini/hooks.js +57 -16
- package/src/adapters/gemini/index.js +57 -18
- package/src/adapters/kiro/index.js +15 -1
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/index.js +34 -9
- package/src/cli/commands/install.js +1 -1
- package/src/cli/commands/update.js +1 -1
- package/src/cli/index.js +17 -5
- package/src/cli/runtime-commands.js +51 -12
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/index.js +28 -14
- package/src/registry/index.js +136 -7
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/claims.js +14 -5
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +35 -268
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1029 -244
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +7 -0
- package/src/runtime/freshness.js +44 -7
- package/src/runtime/outcome.js +1 -1
- package/src/runtime/readiness.js +83 -13
- package/src/runtime/retrieval-plan.js +422 -73
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/artifacts.js +3 -0
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/task-attempts.js +1 -1
- package/src/runtime/task-scope.js +28 -3
- package/src/runtime/task-state.js +72 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
package/src/cli/index.js
CHANGED
|
@@ -70,6 +70,8 @@ function parseArgs(argv ) {
|
|
|
70
70
|
case '--permissions': o.permissions = true; break;
|
|
71
71
|
case '--statusline': o.statusline = true; break;
|
|
72
72
|
case '--json': o.json = true; break;
|
|
73
|
+
case '--bare': o.bare = true; break;
|
|
74
|
+
case '--no-record': o.noRecord = true; break;
|
|
73
75
|
case '--check': o.check = true; break;
|
|
74
76
|
// decision --action list: every decision by id, not just the live ones.
|
|
75
77
|
case '--all': o.all = true; break;
|
|
@@ -136,6 +138,7 @@ function parseArgs(argv ) {
|
|
|
136
138
|
o.node = val; i++; break;
|
|
137
139
|
}
|
|
138
140
|
case '--forced': o.forced = true; break;
|
|
141
|
+
case '--absence': o.absence = true; break;
|
|
139
142
|
case '--decision': {
|
|
140
143
|
const val = argv[i + 1];
|
|
141
144
|
if (val === undefined || val.startsWith('-')) { console.error(`doflow: ${a} requires a value`); process.exit(1); }
|
|
@@ -258,6 +261,9 @@ const RUNTIME_STRING_FLAGS = new Map([
|
|
|
258
261
|
// change scope, <path>[,<path>...], unioned with the plan's bound
|
|
259
262
|
['--invariants', 'invariants'], // readiness: the invariants a refactor must preserve
|
|
260
263
|
['--objective', 'objective'], // context-pack
|
|
264
|
+
['--mandatory-file', 'mandatoryFile'], // context-pack: caller-supplied mandatory fields
|
|
265
|
+
['--max-bytes', 'maxBytes'], // context-pack: per-call cap in bytes
|
|
266
|
+
['--expand', 'expand'], // context-pack: pointer id to expand
|
|
261
267
|
['--stage-id', 'stageId'], // research-request: active feature stage
|
|
262
268
|
['--question', 'question'], // research-request: external question
|
|
263
269
|
['--blocking', 'blocking'], // research-request: true | false
|
|
@@ -266,6 +272,7 @@ const RUNTIME_STRING_FLAGS = new Map([
|
|
|
266
272
|
['--gap', 'gap'], // research-request: unresolved reason
|
|
267
273
|
['--stage', 'stage'], // retrieval-plan: the stage id declaring the plan;
|
|
268
274
|
// outcome: the stage writing it, refused unless terminal
|
|
275
|
+
['--status', 'attemptStatus'], // retrieval-plan: attempt status (EMPTY|UNREACHED|UNVERIFIED)
|
|
269
276
|
['--state', 'state'], // outcome: the terminal state being recorded
|
|
270
277
|
['--result', 'result'], // orchestrate complete-stage: the stage's own outcome, passed | failed (omit → unverified)
|
|
271
278
|
// outcome: the verdicts the run saw, stated by it and validated against the vocabulary the
|
|
@@ -281,6 +288,8 @@ const RUNTIME_STRING_FLAGS = new Map([
|
|
|
281
288
|
['--owner', 'owner'], // brief-tools: the agent type the brief is for
|
|
282
289
|
['--tier', 'tier'], // brief-tools: the task line's tier (light | standard | frontier)
|
|
283
290
|
['--harness', 'harness'], // brief-tools: the harness the brief is for
|
|
291
|
+
['--policy', 'policy'], // enforcement: lifecycle policy id
|
|
292
|
+
['--profile', 'profile'], // enforcement: coverage profile filter
|
|
284
293
|
['--heading-level', 'headingLevel'], // brief-tools: 2 (default) or 3
|
|
285
294
|
['--tasks', 'tasks'], // brief-tools: comma-separated plan task ids for tier advice
|
|
286
295
|
['--plan-path', 'planPath'], // verify: a feature plan.md whose doflow-verification
|
|
@@ -388,12 +397,13 @@ Commands:
|
|
|
388
397
|
self-update git pull + reinstall
|
|
389
398
|
tools Inspect or manage registered external tools
|
|
390
399
|
capabilities Show registered abstract capabilities and resolved providers
|
|
400
|
+
enforcement Report lifecycle policy enforcement coverage (--harness, --policy)
|
|
391
401
|
doctor System health and capability smoke check diagnostics
|
|
392
402
|
readiness Evaluate task readiness contract (--task-class, --task-id, both required)
|
|
393
403
|
evidence Record a stage's evidence batch, or inspect what is recorded (--task-id)
|
|
394
404
|
claim Record a claim, link evidence to it, or list them (--task-id, --action)
|
|
395
405
|
context-pack Compile a task's evidence and claims into a context block (--task-id)
|
|
396
|
-
retrieval-plan Declare a stage's information needs, then report every declared item
|
|
406
|
+
retrieval-plan Declare a stage's information needs, attempt retrieval, then report every declared item
|
|
397
407
|
outcome Record a task's terminal state with its basis, or show it (--task-id)
|
|
398
408
|
classify Validate a proposed task class and return its workflow (--task-class)
|
|
399
409
|
workflow Resolve a task class to its stages, gates and readiness templates
|
|
@@ -453,7 +463,7 @@ Runtime verb arguments (accept --flag value or --flag=value):
|
|
|
453
463
|
--task-class classify, workflow, readiness, context-pack, outcome --action record
|
|
454
464
|
--task-id readiness, evidence, claim, context-pack, retrieval-plan, outcome, verify, advise
|
|
455
465
|
--action claim: list|add|link|retract|supersede · evidence: list|add|supersede
|
|
456
|
-
verify: report|contract · retrieval-plan: declare|report
|
|
466
|
+
verify: report|contract · retrieval-plan: declare|report|attempt
|
|
457
467
|
outcome: record|show · advise: ask|outcome|configure · tools: see above
|
|
458
468
|
followup: add|list|take|settle|promote|report
|
|
459
469
|
task-attempt: claim|finish|release|status
|
|
@@ -470,7 +480,9 @@ Runtime verb arguments (accept --flag value or --flag=value):
|
|
|
470
480
|
--statement, --claim-id,
|
|
471
481
|
--evidence-id, --relation claim
|
|
472
482
|
--kind, --provenance, --provider,
|
|
473
|
-
--capability, --locator, --content
|
|
483
|
+
--capability, --locator, --content,
|
|
484
|
+
--absence evidence --action add (one item)
|
|
485
|
+
--need <intent> evidence --action add (bind evidence to retrieval needs)
|
|
474
486
|
--batch <file|-> evidence --action add (a stage's JSON batch)
|
|
475
487
|
--evidence-id, --replaced-by evidence --action supersede (retire one item, point
|
|
476
488
|
forward to the item that replaces it)
|
|
@@ -480,7 +492,7 @@ Runtime verb arguments (accept --flag value or --flag=value):
|
|
|
480
492
|
evidence establishes; reported back as such
|
|
481
493
|
--objective context-pack
|
|
482
494
|
--need <intent[,intent]>,
|
|
483
|
-
--stage
|
|
495
|
+
--stage, --status retrieval-plan (--need is repeatable), outcome
|
|
484
496
|
--mcp-outcome <server>=<outcome> orchestrate --action handoff, repeatable: used | nothing-usable |
|
|
485
497
|
skipped:<reason> | unavailable (a server left out only warns)
|
|
486
498
|
--state <COMPLETED|BLOCKED|
|
|
@@ -565,4 +577,4 @@ async function main(argv ) {
|
|
|
565
577
|
}
|
|
566
578
|
}
|
|
567
579
|
|
|
568
|
-
module.exports = { main, COMMANDS, commandName };
|
|
580
|
+
module.exports = { main, COMMANDS, commandName, parseArgs };
|
|
@@ -24,6 +24,8 @@ const path = require('node:path');
|
|
|
24
24
|
const {
|
|
25
25
|
handleCapabilitiesCommand, handleReadinessCommand, handleEvidenceCommand,
|
|
26
26
|
} = require('../runtime/cli.js');
|
|
27
|
+
const { loadRegistry } = require('../registry/index.js');
|
|
28
|
+
const { enforcementFor, UnknownEnforcementTarget } = require('../lifecycle/coverage.js');
|
|
27
29
|
// `doctor` comes from the health module rather than src/runtime/cli.js: the health-probe report
|
|
28
30
|
// (FR-013) supersedes the presence-check version, and one verb must have one implementation.
|
|
29
31
|
const { handleDoctorCommand } = require('../runtime/health.js');
|
|
@@ -58,7 +60,7 @@ const { handleStorageCommand } = require('../runtime/storage/cli.js');
|
|
|
58
60
|
function handleFailureCommand(options ) { return require('../runtime/failure/cli.js').handleFailureCommand(options); }
|
|
59
61
|
const { handleInventoryCommand } = require('../runtime/inventory/index.js');
|
|
60
62
|
const { finishRuntime, usageError } = require('../runtime/cli-result.js');
|
|
61
|
-
const { setDefaultSlug, invalidSlugRefusal, slugNamesNoFeature } = require('../runtime/task-scope.js');
|
|
63
|
+
const { setDefaultSlug, invalidSlugRefusal, slugNamesNoFeature, resolveFeature } = require('../runtime/task-scope.js');
|
|
62
64
|
const { listCheckouts } = require('../runtime/checkouts.js');
|
|
63
65
|
const { featureSlugFor } = require('../runtime/implementation-gate.js');
|
|
64
66
|
const { handleBriefToolsCommand } = require('../runtime/brief-tools.js');
|
|
@@ -111,6 +113,9 @@ function evidenceItemFromFlags(o ) {
|
|
|
111
113
|
if (o.locator !== undefined) item.locator = o.locator;
|
|
112
114
|
if (o.content !== undefined) item.content = o.content;
|
|
113
115
|
if (o.establishes !== undefined) item.establishes = o.establishes;
|
|
116
|
+
if (o.need !== undefined) item.need = o.need;
|
|
117
|
+
if (o.result !== undefined) item.result = o.result;
|
|
118
|
+
if (o.absence !== undefined) item.absence = o.absence;
|
|
114
119
|
// Both halves of an observation travel together; a lone half must reach the write boundary as
|
|
115
120
|
// the partial object it is, so the refusal names the missing field instead of dropping the pair.
|
|
116
121
|
if (o.observedCommand !== undefined || o.observedExit !== undefined) {
|
|
@@ -128,9 +133,9 @@ function evidenceItemFromFlags(o ) {
|
|
|
128
133
|
/** Verbs that read or write per-task records, and so accept `--slug` (IC-002). */
|
|
129
134
|
const TASK_STORE_VERBS = new Set(['evidence', 'claim', 'readiness', 'context-pack', 'research-request', 'outcome', 'retrieval-plan']);
|
|
130
135
|
|
|
131
|
-
/** Verbs
|
|
132
|
-
*
|
|
133
|
-
const FEATURE_STORE_VERBS = new Set(['evidence', 'claim', 'readiness']);
|
|
136
|
+
/** Verbs whose task records live beside the feature folder when run from a linked worktree that
|
|
137
|
+
* does not contain it. */
|
|
138
|
+
const FEATURE_STORE_VERBS = new Set(['evidence', 'claim', 'readiness', 'retrieval-plan']);
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
141
|
* The main checkout, when `o` runs one of FEATURE_STORE_VERBS in a linked worktree that lacks the
|
|
@@ -139,11 +144,16 @@ const FEATURE_STORE_VERBS = new Set(['evidence', 'claim', 'readiness']);
|
|
|
139
144
|
* the readiness record written there is the one verify, orchestrate and the edit hook read from
|
|
140
145
|
* the worktree.
|
|
141
146
|
*/
|
|
142
|
-
function featureStoreRoot(o ) {
|
|
147
|
+
function featureStoreRoot(o , checkouts ) {
|
|
143
148
|
if (!FEATURE_STORE_VERBS.has(o.cmd) || o.global) return null;
|
|
144
|
-
const checkouts = listCheckouts({ cwd: evidenceRoot(o) });
|
|
145
149
|
if (!checkouts.isLinked) return null;
|
|
146
|
-
|
|
150
|
+
let slug = o.slug || null;
|
|
151
|
+
if (!slug) {
|
|
152
|
+
try {
|
|
153
|
+
const feature = resolveFeature({ projectRoot: checkouts.current });
|
|
154
|
+
slug = !feature.error && feature.paths ? feature.paths.feature_slug : null;
|
|
155
|
+
} catch { /* a failed branch lookup cannot route state to a guessed feature */ }
|
|
156
|
+
}
|
|
147
157
|
const folder = (root ) => path.join(root, 'agent-docs', 'doflow', slug || '');
|
|
148
158
|
if (!slug || fs.existsSync(folder(checkouts.current)) || !fs.existsSync(folder(checkouts.main))) return null;
|
|
149
159
|
return { root: checkouts.main, slug };
|
|
@@ -169,8 +179,11 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
169
179
|
// here so the verbs whose handlers build their own ledger (readiness, evidence) route the same
|
|
170
180
|
// way as the ones that take `slug` directly. The orchestration journal is keyed by slug already.
|
|
171
181
|
// Reset on every dispatch, so one invocation's slug can never carry into the next in-process call.
|
|
172
|
-
const
|
|
182
|
+
const checkouts = FEATURE_STORE_VERBS.has(o.cmd) && !o.global
|
|
183
|
+
? listCheckouts({ cwd: evidenceRoot(o) }) : null;
|
|
184
|
+
const featureStore = featureStoreRoot(o, checkouts);
|
|
173
185
|
const storeRoot = featureStore ? featureStore.root : evidenceRoot(o);
|
|
186
|
+
const attemptRoot = checkouts?.ok && !checkouts.sandbox ? (checkouts.main || checkouts.current || null) : null;
|
|
174
187
|
// Use the same root resolver as the handler: task redirection, the nearest run ledger,
|
|
175
188
|
// and lifecycle's shared main-worktree store are deliberately different scopes.
|
|
176
189
|
try {
|
|
@@ -197,6 +210,7 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
197
210
|
// REPO_ROOT locates the capability registry; projectRoot is the tree whose index freshness
|
|
198
211
|
// and build/test commands are being reported on, which follows the usual scope rules.
|
|
199
212
|
case 'capabilities': return handleCapabilitiesCommand({ json: o.json, check: o.check, repoRoot: REPO_ROOT });
|
|
213
|
+
case 'enforcement': return handleEnforcementCommand(o);
|
|
200
214
|
case 'doctor': return handleDoctorCommand({ json: o.json, repoRoot: REPO_ROOT, projectRoot: evidenceRoot(o), adapters: buildAdapterRegistry() });
|
|
201
215
|
// REPO_ROOT locates the registry (templates ship with the package); stateRoot locates the
|
|
202
216
|
// caller's evidence, which follows the same scope rules as every other command: -g means
|
|
@@ -211,8 +225,8 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
211
225
|
// than from evidence, so the gate had one reachable answer for every task. Forwarding is
|
|
212
226
|
// all this does: the handler names them back under `callerAsserted` so a stated input is
|
|
213
227
|
// never mistaken for a measured one.
|
|
214
|
-
case 'readiness': return handleReadinessCommand({ taskClass: requireTaskClass(o), taskId: requireTaskId(o), verificationPlan: o.verificationPlan, scopeClear: o.scope, invariants: o.invariants, userDecisionPending: o.userDecisionPending, mode: o.mode, slug: storeSlug, json: o.json, repoRoot: REPO_ROOT, stateRoot: storeRoot });
|
|
215
|
-
case 'evidence': return handleEvidenceCommand({ taskId: requireTaskId(o), action: o.action, item: evidenceItemFromFlags(o), batchPath: o.batchPath, evidenceId: o.evidenceId, replacedBy: o.replacedBy, json: o.json, repoRoot: REPO_ROOT, stateRoot: storeRoot });
|
|
228
|
+
case 'readiness': return handleReadinessCommand({ taskClass: requireTaskClass(o), taskId: requireTaskId(o), verificationPlan: o.verificationPlan, scopeClear: o.scope, invariants: o.invariants, userDecisionPending: o.userDecisionPending, mode: o.mode, slug: storeSlug, json: o.json, repoRoot: REPO_ROOT, stateRoot: storeRoot, sourceRoot: evidenceRoot(o) });
|
|
229
|
+
case 'evidence': return handleEvidenceCommand({ taskId: requireTaskId(o), action: o.action, item: evidenceItemFromFlags(o), batchPath: o.batchPath, evidenceId: o.evidenceId, replacedBy: o.replacedBy, json: o.json, repoRoot: REPO_ROOT, stateRoot: storeRoot, slug: storeSlug, sourceRoot: evidenceRoot(o), attemptRoot });
|
|
216
230
|
// Run-ledger views. They resolve their own ledger the way the dispatcher does (nearest
|
|
217
231
|
// `.doflow` walking up, or the global one) rather than assuming cwd is the project root, so
|
|
218
232
|
// a view invoked from a subdirectory reads the runs that were actually recorded.
|
|
@@ -260,8 +274,8 @@ function dispatchRuntimeCommandInner(o , moment ) {
|
|
|
260
274
|
case 'model-role': return handleModelRoleCommand({ role: o.role, exclude: o.exclude, json: o.json, repoRoot: REPO_ROOT });
|
|
261
275
|
case 'route': return handleRouteCommand({ intent: o.intent, query: o.query, check: o.check, json: o.json, projectRoot: evidenceRoot(o) });
|
|
262
276
|
case 'claim': return handleClaimCommand({ slug: storeSlug, taskId: requireTaskId(o), action: o.action, statement: o.statement, claimId: o.claimId, evidenceId: o.evidenceId, replacedBy: o.replacedBy, relation: o.relation, role: o.role, json: o.json, stateRoot: storeRoot });
|
|
263
|
-
case 'context-pack': return handleContextPackCommand({ taskId: requireTaskId(o), taskClass: o.taskClass, objective: o.objective, json: o.json, stateRoot: evidenceRoot(o), slug: o.slug });
|
|
264
|
-
case 'retrieval-plan': return handleRetrievalPlanCommand({ slug:
|
|
277
|
+
case 'context-pack': return handleContextPackCommand({ taskId: requireTaskId(o), taskClass: o.taskClass, objective: o.objective, json: o.json, stateRoot: evidenceRoot(o), slug: o.slug, mandatoryFile: o.mandatoryFile, harness: o.harness, maxBytes: o.maxBytes, bare: o.bare, noRecord: o.noRecord, reason: o.reason, since: o.since, expand: o.expand });
|
|
278
|
+
case 'retrieval-plan': return handleRetrievalPlanCommand({ slug: storeSlug, taskId: requireTaskId(o), action: o.action, need: o.need, stage: o.stage, provider: o.provider, attemptStatus: o.attemptStatus, reason: o.reason, note: o.note, json: o.json, repoRoot: REPO_ROOT, stateRoot: storeRoot, sourceRoot: evidenceRoot(o) });
|
|
265
279
|
case 'outcome': return handleOutcomeCommand({ slug: o.slug, taskId: requireTaskId(o), action: o.action, state: o.state, taskClass: o.taskClass, stage: o.stage, readiness: o.readiness, verification: o.verification, json: o.json, repoRoot: REPO_ROOT, stateRoot: evidenceRoot(o) });
|
|
266
280
|
case 'verify': return handleVerifyCommand({ slug: o.slug, taskId: requireTaskId(o), action: o.action, risk: o.risk, planPath: o.planPath, scope: o.scope, verificationInput: o.verificationInput, verificationPhase: o.verificationPhase, baselineRef: o.baselineRef, json: o.json, projectRoot: evidenceRoot(o) });
|
|
267
281
|
case 'leak-scan': return handleLeakScanCommand({ paths: o.paths, exclude: o.exclude, json: o.json, repoRoot: evidenceRoot(o) });
|
|
@@ -278,4 +292,29 @@ async function dispatchRuntimeCommand(o ) {
|
|
|
278
292
|
return result;
|
|
279
293
|
}
|
|
280
294
|
|
|
295
|
+
function handleEnforcementCommand(o ) {
|
|
296
|
+
const fail = (error , detail ) => {
|
|
297
|
+
if (o.json) console.log(JSON.stringify({ error, detail }));
|
|
298
|
+
else console.error(`doflow enforcement: ${detail}`);
|
|
299
|
+
process.exitCode = 2;
|
|
300
|
+
return 2;
|
|
301
|
+
};
|
|
302
|
+
if (typeof o.harness !== 'string' || !o.harness || typeof o.policy !== 'string' || !o.policy || (o.profile !== undefined && typeof o.profile !== 'string')) return fail('usage', 'requires --harness <id> --policy <id> [--profile <id>]');
|
|
303
|
+
try {
|
|
304
|
+
const registry = loadRegistry({ repoRoot: REPO_ROOT });
|
|
305
|
+
const answer = enforcementFor(registry.coverage, { harness: o.harness, policy: o.policy, ...(o.profile ? { profile: o.profile } : {}) });
|
|
306
|
+
if (o.json) console.log(JSON.stringify(answer));
|
|
307
|
+
else {
|
|
308
|
+
console.log(`${answer.harness} ${answer.policy}: ${answer.enforced ? 'enforced' : 'not enforced'} (${answer.class}, ${answer.verification})`);
|
|
309
|
+
for (const item of answer.doesNotCover) console.log(`does not cover: ${item}`);
|
|
310
|
+
for (const item of answer.prerequisites) console.log(`prerequisite: ${item}`);
|
|
311
|
+
}
|
|
312
|
+
process.exitCode = answer.enforced ? 0 : 1;
|
|
313
|
+
return process.exitCode;
|
|
314
|
+
} catch (error ) {
|
|
315
|
+
if (error instanceof UnknownEnforcementTarget) return fail(error.code, error.message);
|
|
316
|
+
return fail('usage', error.message);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
281
320
|
module.exports = { dispatchRuntimeCommand };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
|
|
5
|
+
const POLICY_ORDER = Object.freeze(['pre-implementation-gate', 'pre-bash-guard', 'session-context', 'mcp-tool-guard', 'stop-check']);
|
|
6
|
+
const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'gemini', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
|
|
7
|
+
const NEVER_BLOCKING_EVENTS = new Set([
|
|
8
|
+
'pi\u0000tool_execution_start', 'antigravity\u0000PostToolUse', 'antigravity\u0000PreInvocation',
|
|
9
|
+
'antigravity\u0000PostInvocation', 'gemini\u0000AfterTool', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
|
|
10
|
+
]);
|
|
11
|
+
function stable(value ) {
|
|
12
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
13
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
function rowsDigest(rows ) { return `sha256:${crypto.createHash('sha256').update(stable(rows)).digest('hex')}`; }
|
|
17
|
+
class UnknownEnforcementTarget extends Error {
|
|
18
|
+
code ;
|
|
19
|
+
constructor(code , detail ) { super(detail); this.name = 'UnknownEnforcementTarget'; this.code = code; }
|
|
20
|
+
}
|
|
21
|
+
function enforcementFor(coverage , query ) {
|
|
22
|
+
const rows = coverage?.rows || [];
|
|
23
|
+
const profiles = coverage?.profiles || {};
|
|
24
|
+
if (query.policy === '*') throw new UnknownEnforcementTarget('unknown-policy', "Unknown policy '*'");
|
|
25
|
+
if (!rows.some((row ) => row.harness === query.harness)) throw new UnknownEnforcementTarget('unknown-harness', `Unknown harness '${query.harness}'`);
|
|
26
|
+
if (!rows.some((row ) => row.policy === query.policy)) throw new UnknownEnforcementTarget('unknown-policy', `Unknown policy '${query.policy}'`);
|
|
27
|
+
if (query.profile !== undefined && !Object.hasOwn(profiles, query.profile)) throw new UnknownEnforcementTarget('unknown-profile', `Unknown profile '${query.profile}'`);
|
|
28
|
+
const pairRows = rows.filter((row ) => row.harness === query.harness && (row.policy === query.policy || row.policy === '*'));
|
|
29
|
+
const timing = query.policy === 'stop-check' ? 'stop' : 'pre-tool';
|
|
30
|
+
const applicable = pairRows.filter((row ) => row.timing === timing && (query.profile === undefined || row.profile === query.profile || row.profile === 'any'));
|
|
31
|
+
const enforced = applicable.length > 0 && applicable.every((row ) => row.wired && row.class === 'blocking' && row.verification === 'verified');
|
|
32
|
+
const rank = (row ) => CLASS_ORDER.indexOf(row.class);
|
|
33
|
+
const weakest = [...applicable].sort((a , b ) => rank(a) - rank(b))[0];
|
|
34
|
+
const intersection = (field ) => applicable.length ? [...new Set (applicable[0][field] || [])].filter((item) => applicable.every((row ) => (row[field] || []).includes(item))) : [];
|
|
35
|
+
const union = (field ) => [...new Set(applicable.flatMap((row ) => row[field] || []))];
|
|
36
|
+
const parity = enforced && pairRows.length > 0 && pairRows.every((row ) => row.class === 'blocking');
|
|
37
|
+
return {
|
|
38
|
+
harness: query.harness, policy: query.policy, profile: query.profile ?? null, enforced, parity,
|
|
39
|
+
class: weakest?.class || 'unknown', verification: weakest?.verification || 'unverified',
|
|
40
|
+
prerequisites: union('prerequisites'), covers: intersection('covers'), doesNotCover: union('doesNotCover'), caveats: union('caveats'),
|
|
41
|
+
rows: applicable, matrix: { schemaVersion: coverage.schemaVersion, revision: coverage.revision, rowsDigest: coverage.rowsDigest },
|
|
42
|
+
reason: weakest ? `${weakest.class} at (${weakest.policy}, ${weakest.harness}, ${weakest.event}, ${weakest.profile})` : `unknown at (${query.policy}, ${query.harness}, *, ${query.profile ?? '*'})`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function renderEnforcementMatrix(coverage ) {
|
|
46
|
+
const order = (items , value ) => {
|
|
47
|
+
const index = items.indexOf(value);
|
|
48
|
+
return index < 0 ? items.length : index;
|
|
49
|
+
};
|
|
50
|
+
const rows = [...coverage.rows].sort((a , b ) => order(POLICY_ORDER, a.policy) - order(POLICY_ORDER, b.policy)
|
|
51
|
+
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
52
|
+
|| a.event.localeCompare(b.event) || a.profile.localeCompare(b.profile));
|
|
53
|
+
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}; ${coverage.rowsDigest}`, '| Policy | Harness | Native event | Profile | Class | Verification | Prerequisites | Does not cover |', '|---|---|---|---|---|---|---|---|'];
|
|
54
|
+
for (const row of rows) lines.push(`| ${row.policy} | ${row.harness} | ${row.event} | ${row.profile} | ${row.class} | ${row.verification} | ${(row.prerequisites || []).join(', ')} | ${(row.doesNotCover || []).join(', ')} |`);
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
module.exports = { enforcementFor, renderEnforcementMatrix, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// IC-037 (DEC-034): the shared hooks tree and the hook front doors of every harness that reads it
|
|
6
|
+
// move together. An install or update that writes changed bytes into `hooks.shared` also plans the
|
|
7
|
+
// front doors of the other harnesses installed in the same scope, so a harness the run does not
|
|
8
|
+
// name is never left with doors written for an older tree. The doors ride the same plan, backup
|
|
9
|
+
// and ledger write as the run's own targets, and a door edited outside DoFlow refuses the whole plan.
|
|
10
|
+
const { selectAssets } = require('../registry/index.js');
|
|
11
|
+
const { resolveAdapter, projectAdapterInput } = require('../adapters/index.js');
|
|
12
|
+
const { renderPolicies } = require('./policies.js');
|
|
13
|
+
|
|
14
|
+
const HOOKS_TREE_ASSET = 'hooks.shared';
|
|
15
|
+
const OPERATIONS = ['install', 'update'];
|
|
16
|
+
|
|
17
|
+
/** Whether any target of the plan creates or updates a file of the shared hooks tree. */
|
|
18
|
+
function hooksTreeChanged(targets ) {
|
|
19
|
+
return targets.some((target ) => !target.skipped
|
|
20
|
+
&& target.changes.some((change ) => change.assetId === HOOKS_TREE_ASSET && (change.operation === 'create' || change.operation === 'update')));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The plan entries for the hook front doors of every harness installed in this scope that the run
|
|
24
|
+
* does not name. Only the doors that differ from the current source appear, each with one notice;
|
|
25
|
+
* a door the adapter refuses to overwrite carries the conflict and the command that resolves it.
|
|
26
|
+
* `harnessScope` is the registry's name for the scope. `targetFromResult` is the lifecycle's own
|
|
27
|
+
* conversion of an adapter result into a plan entry. */
|
|
28
|
+
function planHookCompanions({ registry, adapters, scope, harnessScope, scopeRoot, ledger, context, targets, targetFromResult } ) {
|
|
29
|
+
// DEC-034 is about install and update: every other operation plans exactly the targets it was asked for.
|
|
30
|
+
if (!OPERATIONS.includes(context.operation) || !hooksTreeChanged(targets)) return [];
|
|
31
|
+
const named = new Set(targets.map((target ) => target.harness));
|
|
32
|
+
const installed = new Set((ledger?.resources ?? []).map((resource ) => resource.harness));
|
|
33
|
+
const run = targets.filter((target ) => !target.skipped).map((target ) => target.harness).join(',');
|
|
34
|
+
const companions = [];
|
|
35
|
+
for (const harness of registry.harnesses) {
|
|
36
|
+
if (named.has(harness.id) || !installed.has(harness.id) || !harness.scopes.includes(harnessScope)) continue;
|
|
37
|
+
if (!harness.capabilities?.hooks || harness.capabilities.hooks.status === 'unavailable') continue;
|
|
38
|
+
const adapter = resolveAdapter(adapters, harness);
|
|
39
|
+
if (typeof adapter.planHookDoors !== 'function') continue;
|
|
40
|
+
const assets = selectAssets(registry, { harness: harness.id });
|
|
41
|
+
const policies = renderPolicies(registry, { harness: harness.id });
|
|
42
|
+
const adapterInput = projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies, context });
|
|
43
|
+
const input = { ...adapterInput, registry, ledger };
|
|
44
|
+
const discovery = adapter.discover(input);
|
|
45
|
+
const target = targetFromResult({ harness, assets, mcp: [], mcpSelected: null, policies, adapterInput, discovery,
|
|
46
|
+
result: adapter.planHookDoors({ ...input, discovery }) });
|
|
47
|
+
if (!target.changes.length && !target.conflicts.length) continue;
|
|
48
|
+
companions.push({
|
|
49
|
+
...target, hookDoorsOnly: true,
|
|
50
|
+
// The assets that own the doors this run plans, not the harness's whole set.
|
|
51
|
+
assets: target.assets.filter((asset ) => target.changes.some((change ) => change.assetId === asset.id)),
|
|
52
|
+
conflicts: target.conflicts.length
|
|
53
|
+
? [...target.conflicts, `${harness.id} hook front doors must move with the shared hooks tree; run: doflow update -t ${run},${harness.id} (add -f to overwrite edited doors). Working command: doflow update -t ${run},${harness.id} -f`]
|
|
54
|
+
: target.conflicts,
|
|
55
|
+
notices: target.changes.length ? [`refreshed ${harness.id} hook front doors to match the shared hooks tree`, ...target.notices] : target.notices,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return companions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { planHookCompanions };
|
package/src/lifecycle/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const { renderPolicies } = require('./policies.js');
|
|
|
16
16
|
const { renderMcpIndex } = require('./mcp-index.js');
|
|
17
17
|
const { hasBashCapableShell } = require('./bash-availability.js');
|
|
18
18
|
const { hookTrustFor: geminiHookTrustFor } = require('../adapters/gemini/index.js');
|
|
19
|
+
const { planHookCompanions } = require('./hook-step.js');
|
|
19
20
|
|
|
20
21
|
const OPERATIONS = new Set(['create', 'merge', 'update', 'remove']);
|
|
21
22
|
/** A harness's MCP selection meaning "the servers it owns now", read through its adapter's
|
|
@@ -453,6 +454,18 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
453
454
|
: withChanges(target, replaced.get(target.harness) , added.get(target.harness) , notices.get(target.harness) )));
|
|
454
455
|
}
|
|
455
456
|
|
|
457
|
+
/** One harness's plan entry, from what its adapter returned: the changes checked against the
|
|
458
|
+
* harness's assets, and the conflicts, prerequisites and notices in the lifecycle's common shape. */
|
|
459
|
+
function targetFromResult({ harness, assets, mcp, mcpSelected, policies, adapterInput, discovery, result } ) {
|
|
460
|
+
if (!result || typeof result !== 'object') throw new Error(`Adapter '${harness.id}' returned an invalid plan`);
|
|
461
|
+
const changes = (result.changes || []).map((change ) => normalizeChange(change, { harness, assets }));
|
|
462
|
+
const conflicts = adapterConflicts(result, harness);
|
|
463
|
+
const prerequisites = [...(result.prerequisites || []), ...changes.map((change ) => change.prerequisite).filter(Boolean)];
|
|
464
|
+
const requiredNativeResources = result.requiredNativeResources ?? changes;
|
|
465
|
+
if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
|
|
466
|
+
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
456
469
|
function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], ledger, context = {} } ) {
|
|
457
470
|
assertScope(scope);
|
|
458
471
|
if (!registry) throw new Error('registry is required');
|
|
@@ -478,13 +491,7 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
478
491
|
const input = { ...adapterInput, registry, ledger: baseLedger };
|
|
479
492
|
const discovery = adapter.discover(input);
|
|
480
493
|
const result = adapter.plan({ ...input, discovery });
|
|
481
|
-
|
|
482
|
-
const changes = (result.changes || []).map((change ) => normalizeChange(change, { harness, assets }));
|
|
483
|
-
const conflicts = adapterConflicts(result, harness);
|
|
484
|
-
const prerequisites = [...(result.prerequisites || []), ...changes.map((change ) => change.prerequisite).filter(Boolean)];
|
|
485
|
-
const requiredNativeResources = result.requiredNativeResources ?? changes;
|
|
486
|
-
if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
|
|
487
|
-
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
494
|
+
return targetFromResult({ harness, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, result });
|
|
488
495
|
});
|
|
489
496
|
// The MCP index names every server a harness of this scope uses: the selections planned here and
|
|
490
497
|
// the recorded selections of the harnesses this run does not touch.
|
|
@@ -495,12 +502,17 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
495
502
|
const retained = markRetainedRemovals(harnessPlans, baseLedger, scope);
|
|
496
503
|
const annotated = context.operation === 'remove' ? retained
|
|
497
504
|
: pruneRetiredRuntimeFiles(retained, baseLedger, { scopeRoot, repoRoot: context.repoRoot });
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
505
|
+
// The shared hooks tree may have moved under harnesses this run does not name (IC-037): their
|
|
506
|
+
// front doors come along, as targets of this same plan.
|
|
507
|
+
const companions = planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
|
|
508
|
+
targets: annotated, targetFromResult });
|
|
509
|
+
const planned = [...annotated, ...companions];
|
|
510
|
+
const changes = planned.flatMap((item ) => item.changes);
|
|
511
|
+
const conflicts = planned.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
|
|
512
|
+
const prerequisites = planned.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
|
|
513
|
+
const requiredNativeResources = planned.flatMap((item ) => item.requiredNativeResources || []);
|
|
514
|
+
const notices = planned.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice })));
|
|
515
|
+
return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: planned, changes, requiredNativeResources, conflicts, prerequisites, notices,
|
|
504
516
|
retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0 });
|
|
505
517
|
}
|
|
506
518
|
|
|
@@ -644,7 +656,9 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
|
|
|
644
656
|
const plannedWrites = context.checkPlannedWrites
|
|
645
657
|
? target.changes.filter((change ) => !change.retained && (change.operation === 'create' || change.operation === 'update')).map((change ) => change.target)
|
|
646
658
|
: undefined;
|
|
647
|
-
|
|
659
|
+
// A hook-front-door companion (IC-037) is verified for its doors alone: the rest of that
|
|
660
|
+
// harness is not part of this run and may legitimately trail the current source.
|
|
661
|
+
let verification = normalizeVerification(adapter[target.hookDoorsOnly ? 'verifyHookDoors' : 'verify']({ ...target.adapterInput,
|
|
648
662
|
registry: context.registry ?? plan.registry, discovery: target.discovery, ledger: plan.ledger,
|
|
649
663
|
operation, context: { ...target.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
|
|
650
664
|
if (operation === 'remove') verification = normalizeRemovalVerification(verification, target.changes, plan.ledger);
|