@khoavu882/doflow 2.0.0-beta.3 → 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/bin/doflow.js +2 -2
- 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 +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- 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-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- 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 +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- 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/runtime/context-pack.ts +0 -492
|
@@ -12,23 +12,23 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('node:fs');
|
|
14
14
|
const path = require('node:path');
|
|
15
|
-
const { WorkflowEngine } = require('./workflow-engine.
|
|
16
|
-
const { updateTaskState, readTaskState } = require('./task-state.
|
|
17
|
-
const { ResearchRequestStore } = require('./research-request.
|
|
18
|
-
const { REPO_ROOT } = require('../helper/repo-root.
|
|
19
|
-
const { resolveActiveFeature } = require('./feature-resolve.
|
|
20
|
-
const { compactDecisions } = require('./decision-register.
|
|
21
|
-
const { EvidenceLedger } = require('./evidence-ledger.
|
|
22
|
-
const { resolveConsultations } = require('./mcp-consultations.
|
|
23
|
-
const { mcpReach } = require('./mcp-reach.
|
|
24
|
-
const { loadRegistry } = require('../registry/index.
|
|
25
|
-
const { loadStorageRegistry } = require('./storage/registry.
|
|
26
|
-
const { resolveRoots, resolveStorePath } = require('./storage/roots.
|
|
27
|
-
const { checkCompletion: checkTaskAttemptCompletion } = require('./task-attempts.
|
|
28
|
-
const { appendOutcome, findPick, readWindow } = require('./advice/record.
|
|
15
|
+
const { WorkflowEngine } = require('./workflow-engine.js');
|
|
16
|
+
const { updateTaskState, readTaskState } = require('./task-state.js');
|
|
17
|
+
const { ResearchRequestStore } = require('./research-request.js');
|
|
18
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
19
|
+
const { resolveActiveFeature } = require('./feature-resolve.js');
|
|
20
|
+
const { compactDecisions } = require('./decision-register.js');
|
|
21
|
+
const { EvidenceLedger } = require('./evidence-ledger.js');
|
|
22
|
+
const { resolveConsultations } = require('./mcp-consultations.js');
|
|
23
|
+
const { mcpReach } = require('./mcp-reach.js');
|
|
24
|
+
const { loadRegistry } = require('../registry/index.js');
|
|
25
|
+
const { loadStorageRegistry } = require('./storage/registry.js');
|
|
26
|
+
const { resolveRoots, resolveStorePath } = require('./storage/roots.js');
|
|
27
|
+
const { checkCompletion: checkTaskAttemptCompletion } = require('./task-attempts.js');
|
|
28
|
+
const { appendOutcome, findPick, readWindow } = require('./advice/record.js');
|
|
29
29
|
const {
|
|
30
30
|
checkReadiness, harnessHookNote, featureSlugFor, runSlugOf, findRunFile,
|
|
31
|
-
} = require('./implementation-gate.
|
|
31
|
+
} = require('./implementation-gate.js');
|
|
32
32
|
|
|
33
33
|
/** Appended to the note of an implementation stage completed with no task-attempt ledger (FR-006). */
|
|
34
34
|
const NO_LEDGER_NOTE = 'No task-attempt ledger exists for this feature, so its tasks were not verified; outcome recorded as unverified.';
|
|
@@ -49,7 +49,7 @@ const RUN_STATES = Object.freeze(['RUNNING', 'AWAITING_GATE', 'COMPLETED', 'REJE
|
|
|
49
49
|
* @param {string} state
|
|
50
50
|
* @returns {string} the same state, once it is known to be one
|
|
51
51
|
*/
|
|
52
|
-
function runState(state
|
|
52
|
+
function runState(state ) {
|
|
53
53
|
if (!RUN_STATES.includes(state)) {
|
|
54
54
|
throw new Error(`Unknown run state '${state}' (expected one of: ${RUN_STATES.join(', ')})`);
|
|
55
55
|
}
|
|
@@ -63,16 +63,16 @@ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
63
63
|
/** Why a handoff with no run and no class records nothing, as the result and the CLI line say. */
|
|
64
64
|
const STANDALONE_REASON = 'no run exists for this task and no --task-class was given, so nothing was recorded and no readiness was required';
|
|
65
65
|
|
|
66
|
-
function assertSafeId(value
|
|
66
|
+
function assertSafeId(value , label ) {
|
|
67
67
|
if (!ID_PATTERN.test(String(value ?? ''))) throw new Error(`Invalid ${label}: '${value}'`);
|
|
68
68
|
return String(value);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function iso(now
|
|
71
|
+
function iso(now ) { return (now ?? new Date()).toISOString(); }
|
|
72
72
|
|
|
73
73
|
/** Interleave stages with their gates: each gate sits immediately after the stage named by its
|
|
74
74
|
* `afterStage`. A gate anchored to an unknown stage is a compile error here, not a silent orphan. */
|
|
75
|
-
function compileProgram(workflow
|
|
75
|
+
function compileProgram(workflow ) {
|
|
76
76
|
const gates = [...(workflow.gates || [])];
|
|
77
77
|
const nodes = [];
|
|
78
78
|
for (const stage of workflow.stages || []) {
|
|
@@ -99,15 +99,15 @@ function compileProgram(workflow: any) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
class WorkflowOrchestrator {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
111
|
/**
|
|
112
112
|
* @param {Object} [options]
|
|
113
113
|
* @param {string} [options.repoRoot] install root owning core/registry + default state dir.
|
|
@@ -128,7 +128,7 @@ class WorkflowOrchestrator {
|
|
|
128
128
|
* @param {() => object} [options.buildAdapters] builds the adapter registry `mcpReach` reads installs
|
|
129
129
|
* through; without it install facts are unknown and no outcome is inferred from them
|
|
130
130
|
*/
|
|
131
|
-
constructor({ repoRoot = REPO_ROOT, projectRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, fsImpl, featureSlug = null, buildAdapters = null }
|
|
131
|
+
constructor({ repoRoot = REPO_ROOT, projectRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, fsImpl, featureSlug = null, buildAdapters = null } = {}) {
|
|
132
132
|
this.fsImpl = fsImpl || fs;
|
|
133
133
|
this.repoRoot = repoRoot;
|
|
134
134
|
this.projectRoot = projectRoot || (stateDir?.endsWith(path.join('.doflow', 'state', 'orchestration'))
|
|
@@ -141,21 +141,21 @@ class WorkflowOrchestrator {
|
|
|
141
141
|
this.buildAdapters = buildAdapters;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
runFile(taskId
|
|
144
|
+
runFile(taskId ) {
|
|
145
145
|
return path.join(this.stateDir, `${assertSafeId(taskId, 'taskId')}.json`);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
readRun(taskId
|
|
148
|
+
readRun(taskId ) {
|
|
149
149
|
const file = this.runFile(taskId);
|
|
150
150
|
if (!this.fsImpl.existsSync(file)) return null;
|
|
151
151
|
return readTaskState(this.fsImpl, file);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
writeRun(run
|
|
154
|
+
writeRun(run , now , beforeWrite ) {
|
|
155
155
|
run.updatedAt = iso(now);
|
|
156
156
|
this.fsImpl.mkdirSync(this.stateDir, { recursive: true });
|
|
157
157
|
const expectedRevision = run.revision || 0;
|
|
158
|
-
updateTaskState({ file: this.runFile(run.taskId), fsImpl: this.fsImpl, build: (disk
|
|
158
|
+
updateTaskState({ file: this.runFile(run.taskId), fsImpl: this.fsImpl, build: (disk ) => {
|
|
159
159
|
if ((disk?.revision || 0) !== expectedRevision) {
|
|
160
160
|
throw new Error(`Concurrent workflow update for '${run.taskId}'; reload the run before retrying. Nothing was written.`);
|
|
161
161
|
}
|
|
@@ -167,7 +167,7 @@ class WorkflowOrchestrator {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/** Begin a new run. An existing run for the same task id is refused — resume instead. */
|
|
170
|
-
start({ taskId, taskClass, now }
|
|
170
|
+
start({ taskId, taskClass, now } = {}) {
|
|
171
171
|
assertSafeId(taskId, 'taskId');
|
|
172
172
|
if (this.readRun(taskId)) throw new Error(`Workflow run '${taskId}' already exists; resume it instead of restarting`);
|
|
173
173
|
const workflow = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly
|
|
@@ -190,9 +190,9 @@ class WorkflowOrchestrator {
|
|
|
190
190
|
return this.snapshot(run);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
snapshot(run
|
|
193
|
+
snapshot(run ) {
|
|
194
194
|
const current = run.cursor < run.program.length ? run.program[run.cursor] : null;
|
|
195
|
-
const done = run.program.filter((n
|
|
195
|
+
const done = run.program.filter((n ) => n.status === 'completed' || n.status === 'skipped' || n.status === 'approved').length;
|
|
196
196
|
return {
|
|
197
197
|
taskId: run.taskId,
|
|
198
198
|
taskClass: run.taskClass,
|
|
@@ -205,7 +205,7 @@ class WorkflowOrchestrator {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
status(taskId
|
|
208
|
+
status(taskId ) {
|
|
209
209
|
const run = this.requireRun(taskId);
|
|
210
210
|
return this.snapshot(run);
|
|
211
211
|
}
|
|
@@ -217,7 +217,7 @@ class WorkflowOrchestrator {
|
|
|
217
217
|
* entry this handoff writes, and returned as `consultations` and `mcpWarnings`. Only a handoff
|
|
218
218
|
* that completes a stage or annotates one stores a record; a standalone one reports and stores
|
|
219
219
|
* nothing, a deferred one does neither. Nothing in that path can fail or refuse the handoff. */
|
|
220
|
-
handoff({ taskId, taskClass, callingSkill, note, outcome, mcpOutcomes, now }
|
|
220
|
+
handoff({ taskId, taskClass, callingSkill, note, outcome, mcpOutcomes, now } = {}) {
|
|
221
221
|
assertSafeId(taskId, 'taskId');
|
|
222
222
|
assertSafeId(callingSkill, 'callingSkill');
|
|
223
223
|
if (!note || !String(note).trim()) throw new Error('handoff requires a note describing the work recorded');
|
|
@@ -232,9 +232,9 @@ class WorkflowOrchestrator {
|
|
|
232
232
|
}
|
|
233
233
|
if (existing && taskClass && taskClass !== existing.taskClass) throw new Error(`Task class '${taskClass}' does not match run '${existing.taskClass}'`);
|
|
234
234
|
const resolvedClass = existing?.taskClass || taskClass;
|
|
235
|
-
const stages = existing ? existing.program.filter((n
|
|
235
|
+
const stages = existing ? existing.program.filter((n ) => n.type === 'stage')
|
|
236
236
|
: this.engine.resolveWorkflow(resolvedClass).stages;
|
|
237
|
-
const candidates = stages.filter((n
|
|
237
|
+
const candidates = stages.filter((n ) => n.skill === callingSkill).map((n ) => n.id);
|
|
238
238
|
if (!candidates.length) throw new Error(`Skill '${callingSkill}' owns no stage in '${resolvedClass}'`);
|
|
239
239
|
// The stage this handoff would complete is known before anything is written, so a gated stage
|
|
240
240
|
// without readiness is refused before catch-up starts a run or backfills a stage.
|
|
@@ -251,7 +251,7 @@ class WorkflowOrchestrator {
|
|
|
251
251
|
const since = existing ? this.stageSince(taskId, positioned.caughtUpTo) : null;
|
|
252
252
|
const mcp = this.resolveMcp({ taskId, stageId: positioned.caughtUpTo, skill: callingSkill, flags: mcpOutcomes, since });
|
|
253
253
|
const consultations = mcp ? this.storeToolsUsed(mcp) : null;
|
|
254
|
-
const snapshot
|
|
254
|
+
const snapshot = this.completeStage({ taskId, stageId: positioned.caughtUpTo, note, outcome, consultations, now });
|
|
255
255
|
return { ...snapshot, disposition: 'completed', recordedStage: positioned.caughtUpTo, consultations, mcpWarnings: mcp ? mcp.messages : [] };
|
|
256
256
|
}
|
|
257
257
|
if (positioned.reason.startsWith('already-completed:')) {
|
|
@@ -275,12 +275,12 @@ class WorkflowOrchestrator {
|
|
|
275
275
|
* contributes nothing, and a registry that cannot load means no record at all (NFR-003).
|
|
276
276
|
* @returns {{record: object, messages: object[], evidenceToAdd: object[], ledger: EvidenceLedger}|null}
|
|
277
277
|
*/
|
|
278
|
-
resolveMcp({ taskId, stageId, skill, flags, since = null }
|
|
278
|
+
resolveMcp({ taskId, stageId, skill, flags, since = null } ) {
|
|
279
279
|
let registry;
|
|
280
280
|
try { registry = loadRegistry({ repoRoot: this.repoRoot }); } catch { return null; }
|
|
281
281
|
const ledger = new EvidenceLedger({ repoRoot: this.projectRoot, slug: this.featureSlug, fsImpl: this.fsImpl });
|
|
282
282
|
const loaded = new Set();
|
|
283
|
-
let reachMemo
|
|
283
|
+
let reachMemo ;
|
|
284
284
|
const reachOf = () => {
|
|
285
285
|
if (reachMemo === undefined) {
|
|
286
286
|
try {
|
|
@@ -291,8 +291,8 @@ class WorkflowOrchestrator {
|
|
|
291
291
|
}
|
|
292
292
|
return reachMemo;
|
|
293
293
|
};
|
|
294
|
-
const displayName = (id
|
|
295
|
-
const harness = (registry.harnesses || []).find((item
|
|
294
|
+
const displayName = (id ) => {
|
|
295
|
+
const harness = (registry.harnesses || []).find((item ) => item.id === id);
|
|
296
296
|
return harness ? harness.displayName || harness.id : id;
|
|
297
297
|
};
|
|
298
298
|
const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.projectRoot };
|
|
@@ -300,31 +300,31 @@ class WorkflowOrchestrator {
|
|
|
300
300
|
try {
|
|
301
301
|
const result = resolveConsultations({
|
|
302
302
|
taskId, stageId, skill, flags, registry, ledger: {
|
|
303
|
-
queryEvidence: ({ taskId: id }
|
|
303
|
+
queryEvidence: ({ taskId: id } ) => {
|
|
304
304
|
if (!loaded.has(id)) { loaded.add(id); ledger.load(id); }
|
|
305
305
|
return ledger.queryEvidence({ taskId: id });
|
|
306
306
|
},
|
|
307
307
|
},
|
|
308
308
|
requests: new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
|
|
309
309
|
briefRecords,
|
|
310
|
-
readReport: (file
|
|
310
|
+
readReport: (file ) => {
|
|
311
311
|
const target = path.resolve(repoRoot, file);
|
|
312
312
|
// A brief record is project data: a report path that leaves the project is not read.
|
|
313
313
|
if (path.relative(repoRoot, target).startsWith('..') || path.isAbsolute(path.relative(repoRoot, target))) return null;
|
|
314
314
|
try { return this.fsImpl.readFileSync(target, 'utf8'); } catch { return null; }
|
|
315
315
|
},
|
|
316
316
|
reach: {
|
|
317
|
-
anyHarnessHas: (server
|
|
318
|
-
harnessWith: (server
|
|
319
|
-
const row = reachOf()?.harnesses.find((item
|
|
317
|
+
anyHarnessHas: (server ) => { const reach = reachOf(); return reach ? reach.anyHarnessHas(server) : null; },
|
|
318
|
+
harnessWith: (server ) => {
|
|
319
|
+
const row = reachOf()?.harnesses.find((item ) => ['selected', 'configured'].includes(item.servers[server]));
|
|
320
320
|
return row ? displayName(row.id) : null;
|
|
321
321
|
},
|
|
322
322
|
},
|
|
323
323
|
harnessName: displayName,
|
|
324
324
|
since,
|
|
325
|
-
addCommand: (server
|
|
325
|
+
addCommand: (server ) => {
|
|
326
326
|
const reach = reachOf();
|
|
327
|
-
const row = reach?.harnesses.find((item
|
|
327
|
+
const row = reach?.harnesses.find((item ) => !['selected', 'configured'].includes(item.servers[server]));
|
|
328
328
|
return row ? reach.addCommand(row.id, row.scope, server) : null;
|
|
329
329
|
},
|
|
330
330
|
});
|
|
@@ -335,7 +335,7 @@ class WorkflowOrchestrator {
|
|
|
335
335
|
/** When this stage run began, for counting its evidence (IC-008 rule 3): the `at` of the last
|
|
336
336
|
* stage completed or skipped before it that was not backfilled, else the run start. A stage
|
|
337
337
|
* already completed (an annotation) is measured from the completion before its own. */
|
|
338
|
-
stageSince(taskId
|
|
338
|
+
stageSince(taskId , stageId ) {
|
|
339
339
|
try {
|
|
340
340
|
const run = this.readRun(taskId);
|
|
341
341
|
const history = Array.isArray(run?.history) ? run.history : [];
|
|
@@ -354,21 +354,21 @@ class WorkflowOrchestrator {
|
|
|
354
354
|
/** Record advice outcomes for the same brief records the IC-008 handoff pass consumes. This is
|
|
355
355
|
* advisory telemetry only: every storage/read failure is ignored and never affects the handoff.
|
|
356
356
|
*/
|
|
357
|
-
recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot }
|
|
357
|
+
recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot } ) {
|
|
358
358
|
try {
|
|
359
359
|
const storage = loadStorageRegistry({ repoRoot: this.repoRoot });
|
|
360
360
|
const roots = resolveRoots({ registry: storage, projectRoot: repoRoot || this.projectRoot });
|
|
361
|
-
const declaredStores = new Map((storage.stores || []).map((store
|
|
361
|
+
const declaredStores = new Map((storage.stores || []).map((store ) => [store.id, store]));
|
|
362
362
|
const safeRoots = { ...roots };
|
|
363
363
|
for (const id of ['advice-picks-project', 'advice-picks-user']) {
|
|
364
|
-
const store
|
|
364
|
+
const store = declaredStores.get(id);
|
|
365
365
|
if (!store) return;
|
|
366
366
|
if (!resolveStorePath({ store, roots, fsImpl: this.fsImpl })) safeRoots[store.root] = null;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
const existing = readWindow({ roots: safeRoots, fsImpl: this.fsImpl }).records
|
|
370
|
-
.filter(({ record }
|
|
371
|
-
.map(({ record }
|
|
370
|
+
.filter(({ record } ) => record.type === 'outcome')
|
|
371
|
+
.map(({ record } ) => record.pickId);
|
|
372
372
|
const seen = new Set(existing);
|
|
373
373
|
for (const record of briefRecords) {
|
|
374
374
|
const advice = record?.advice;
|
|
@@ -391,7 +391,7 @@ class WorkflowOrchestrator {
|
|
|
391
391
|
/** The brief records (`*-brief.json`) of the feature this run belongs to, parsed; unreadable ones
|
|
392
392
|
* are left out. Resolving the feature spawns the resolver, so it is only done where a feature
|
|
393
393
|
* folder exists above the project. */
|
|
394
|
-
readBriefRecords(taskId
|
|
394
|
+
readBriefRecords(taskId ) {
|
|
395
395
|
const none = { briefRecords: [], repoRoot: this.projectRoot };
|
|
396
396
|
try {
|
|
397
397
|
let dir = path.resolve(this.projectRoot);
|
|
@@ -407,7 +407,7 @@ class WorkflowOrchestrator {
|
|
|
407
407
|
if (feature.error) return none;
|
|
408
408
|
const exec = path.join(feature.featureDir, 'exec');
|
|
409
409
|
const briefRecords = [];
|
|
410
|
-
for (const name of this.fsImpl.readdirSync(exec).filter((entry
|
|
410
|
+
for (const name of this.fsImpl.readdirSync(exec).filter((entry ) => entry.endsWith('-brief.json')).sort()) {
|
|
411
411
|
try { briefRecords.push(JSON.parse(this.fsImpl.readFileSync(path.join(exec, name), 'utf8'))); } catch { /* an unreadable record contributes nothing */ }
|
|
412
412
|
}
|
|
413
413
|
return { briefRecords, repoRoot: feature.repoRoot };
|
|
@@ -418,7 +418,7 @@ class WorkflowOrchestrator {
|
|
|
418
418
|
* its evidence id. Evidence goes in before the stage entry is written, so the entry carries the
|
|
419
419
|
* ids; a retry after a refused completion finds the items and reports them `already-recorded`.
|
|
420
420
|
* A ledger that cannot be written leaves the rows without an id and never stops the handoff. */
|
|
421
|
-
storeToolsUsed({ record, evidenceToAdd, ledger }
|
|
421
|
+
storeToolsUsed({ record, evidenceToAdd, ledger } ) {
|
|
422
422
|
if (!evidenceToAdd.length) return record;
|
|
423
423
|
try {
|
|
424
424
|
const idsByTask = new Map();
|
|
@@ -434,17 +434,17 @@ class WorkflowOrchestrator {
|
|
|
434
434
|
return record;
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
requireRun(taskId
|
|
437
|
+
requireRun(taskId ) {
|
|
438
438
|
const run = this.readRun(taskId);
|
|
439
439
|
if (!run) throw new Error(`No workflow run for task '${taskId}'`);
|
|
440
440
|
return run;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
currentNode(run
|
|
443
|
+
currentNode(run ) {
|
|
444
444
|
return run.cursor < run.program.length ? run.program[run.cursor] : null;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
expectOpenStage(run
|
|
447
|
+
expectOpenStage(run , stageId ) {
|
|
448
448
|
if (run.state !== 'RUNNING') throw new Error(`Run '${run.taskId}' is ${run.state}, not RUNNING`);
|
|
449
449
|
const node = this.currentNode(run);
|
|
450
450
|
if (!node || node.type !== 'stage') throw new Error(`Run '${run.taskId}' has no stage to complete next`);
|
|
@@ -464,7 +464,7 @@ class WorkflowOrchestrator {
|
|
|
464
464
|
* cannot import a verification. A 'failed' outcome still advances — recording the failure
|
|
465
465
|
* honestly beats refusing to record it, and a bug reproduction legitimately completes by
|
|
466
466
|
* observing the expected failure — but the record no longer reads as success. */
|
|
467
|
-
completeStage({ taskId, stageId, note, outcome, backfilled = false, consultations, now }
|
|
467
|
+
completeStage({ taskId, stageId, note, outcome, backfilled = false, consultations, now } = {}) {
|
|
468
468
|
const run = this.requireRun(taskId);
|
|
469
469
|
const node = this.expectOpenStage(run, stageId);
|
|
470
470
|
if (outcome !== undefined && !STAGE_OUTCOMES.has(outcome)) {
|
|
@@ -496,7 +496,7 @@ class WorkflowOrchestrator {
|
|
|
496
496
|
|
|
497
497
|
/** Skip the current OPTIONAL stage without running it. Gates anchored directly to a skipped
|
|
498
498
|
* stage cannot fire, so they are recorded as skipped too rather than blocking forever. */
|
|
499
|
-
skipStage({ taskId, stageId, reason, backfilled = false, now }
|
|
499
|
+
skipStage({ taskId, stageId, reason, backfilled = false, now } = {}) {
|
|
500
500
|
const run = this.requireRun(taskId);
|
|
501
501
|
const node = this.expectOpenStage(run, stageId);
|
|
502
502
|
if (!node.optional) throw new Error(`Stage '${node.id}' is required and cannot be skipped`);
|
|
@@ -539,7 +539,7 @@ class WorkflowOrchestrator {
|
|
|
539
539
|
* (that stage mutates source and only its owner may complete it), or a terminal run state. An
|
|
540
540
|
* optional non-candidate stage is skipped rather than completed, matching what its own skill
|
|
541
541
|
* would have done with it. */
|
|
542
|
-
catchUp({ taskId, taskClass, candidateStageIds, note, now }
|
|
542
|
+
catchUp({ taskId, taskClass, candidateStageIds, note, now } = {}) {
|
|
543
543
|
if (!Array.isArray(candidateStageIds) || candidateStageIds.length === 0) {
|
|
544
544
|
throw new Error('catch-up requires at least one candidate stage id');
|
|
545
545
|
}
|
|
@@ -561,11 +561,11 @@ class WorkflowOrchestrator {
|
|
|
561
561
|
// through them instead of refusing.
|
|
562
562
|
let unknownCandidates;
|
|
563
563
|
if (run) {
|
|
564
|
-
const programStageIds = new Set(run.program.filter((n
|
|
564
|
+
const programStageIds = new Set(run.program.filter((n ) => n.type === 'stage').map((n ) => n.id));
|
|
565
565
|
unknownCandidates = candidateStageIds.filter((id) => !programStageIds.has(id));
|
|
566
566
|
} else {
|
|
567
567
|
const workflow = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly, same as start()
|
|
568
|
-
const workflowStageIds = new Set((workflow.stages || []).map((s
|
|
568
|
+
const workflowStageIds = new Set((workflow.stages || []).map((s ) => s.id));
|
|
569
569
|
unknownCandidates = candidateStageIds.filter((id) => !workflowStageIds.has(id));
|
|
570
570
|
}
|
|
571
571
|
if (unknownCandidates.length > 0) {
|
|
@@ -596,9 +596,9 @@ class WorkflowOrchestrator {
|
|
|
596
596
|
// before the walk below can complete anything past it as a side effect. A caller with more than
|
|
597
597
|
// one candidate (do-test owns two occurrences in `bug`/`refactor`) is not done until every
|
|
598
598
|
// occurrence is: one completed and one still pending must still walk forward to the pending one.
|
|
599
|
-
const candidateNodes = run.program.filter((n
|
|
600
|
-
if (candidateNodes.length > 0 && candidateNodes.every((n
|
|
601
|
-
return { ...this.snapshot(run), caughtUpTo: null, reason: `already-completed:${candidateNodes.map((n
|
|
599
|
+
const candidateNodes = run.program.filter((n ) => candidates.has(n.id));
|
|
600
|
+
if (candidateNodes.length > 0 && candidateNodes.every((n ) => n.status === 'completed' || n.status === 'approved' || n.status === 'skipped')) {
|
|
601
|
+
return { ...this.snapshot(run), caughtUpTo: null, reason: `already-completed:${candidateNodes.map((n ) => n.id).join(',')}` };
|
|
602
602
|
}
|
|
603
603
|
for (;;) {
|
|
604
604
|
if (run.state === 'COMPLETED' || run.state === 'REJECTED') {
|
|
@@ -640,7 +640,7 @@ class WorkflowOrchestrator {
|
|
|
640
640
|
* `forced` decision (e.g. approving despite unresolved clarification markers) requires an
|
|
641
641
|
* explicit `note` reason — never a silent override — and is flagged in history for
|
|
642
642
|
* `render-audit.sh` to distinguish from a routine handoff. */
|
|
643
|
-
decideGate({ taskId, gateId, decision, note, forced = false, now }
|
|
643
|
+
decideGate({ taskId, gateId, decision, note, forced = false, now } = {}) {
|
|
644
644
|
assertSafeId(gateId, 'gateId');
|
|
645
645
|
if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
|
|
646
646
|
if (forced && (!note || !String(note).trim())) throw new Error('A forced gate decision requires a --note reason');
|
|
@@ -667,13 +667,13 @@ class WorkflowOrchestrator {
|
|
|
667
667
|
* hand-edited after that stage's own handoff — without touching program/cursor/state. Not a
|
|
668
668
|
* stage completion or a gate decision, so it never collides with the state machine's ordering
|
|
669
669
|
* rules (D3). `forced` requires an explicit `note` reason, same rule as decideGate. */
|
|
670
|
-
annotate({ taskId, node: nodeId, note, forced = false, consultations, now }
|
|
670
|
+
annotate({ taskId, node: nodeId, note, forced = false, consultations, now } = {}) {
|
|
671
671
|
// Id validated before the run is loaded, matching decideGate's order exactly: an unsafe id is
|
|
672
672
|
// the caller's problem to hear about first, whether or not the run happens to exist.
|
|
673
673
|
assertSafeId(nodeId, 'node');
|
|
674
674
|
if (forced && (!note || !String(note).trim())) throw new Error('A forced annotation requires a --note reason');
|
|
675
675
|
const run = this.requireRun(taskId);
|
|
676
|
-
const found = run.program.find((n
|
|
676
|
+
const found = run.program.find((n ) => n.id === nodeId);
|
|
677
677
|
if (!found) throw new Error(`Invalid node: '${nodeId}' is not part of the program for run '${taskId}'`);
|
|
678
678
|
run.history.push({ at: iso(now), action: 'annotate', node: nodeId, note, forced: Boolean(forced), ...(consultations ? { consultations } : {}) });
|
|
679
679
|
this.writeRun(run, now);
|
|
@@ -682,7 +682,7 @@ class WorkflowOrchestrator {
|
|
|
682
682
|
|
|
683
683
|
/** After any advance: walk past finished nodes, land on the next pending action point, and set
|
|
684
684
|
* the run state from what is found there. Cursor always sits ON the node awaiting action. */
|
|
685
|
-
settleCursor(run
|
|
685
|
+
settleCursor(run ) {
|
|
686
686
|
for (;;) {
|
|
687
687
|
if (run.cursor >= run.program.length) { run.state = runState('COMPLETED'); return; }
|
|
688
688
|
const node = run.program[run.cursor];
|
|
@@ -693,7 +693,7 @@ class WorkflowOrchestrator {
|
|
|
693
693
|
}
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
-
advance(run
|
|
696
|
+
advance(run , now ) {
|
|
697
697
|
run.cursor += 1;
|
|
698
698
|
this.settleCursor(run);
|
|
699
699
|
if (run.state === 'COMPLETED') run.history.push({ at: iso(now), action: 'complete-run' });
|
|
@@ -701,7 +701,7 @@ class WorkflowOrchestrator {
|
|
|
701
701
|
|
|
702
702
|
/** Refuses a gated stage unless the evaluator answers READY. The evaluator returns a verdict
|
|
703
703
|
* string, or `{ verdict, message }`: `message` replaces the generic refusal. */
|
|
704
|
-
assertReady(node
|
|
704
|
+
assertReady(node , run ) {
|
|
705
705
|
if (!this.readinessEvaluate) {
|
|
706
706
|
// Fail closed: without an evaluator there is no way to know the tree is safe to mutate.
|
|
707
707
|
throw new Error(`Stage '${node.id}' is gated by the '${node.readinessTemplate}' readiness template but no readiness evaluator is wired into this orchestrator`);
|
|
@@ -717,7 +717,7 @@ class WorkflowOrchestrator {
|
|
|
717
717
|
/** FR-006: the implementation stage of a run (the `do-execute-plan` stage that mutates source)
|
|
718
718
|
* cannot complete while its feature's task-attempt ledger exists and is incomplete. Returns the
|
|
719
719
|
* checker's answer for a stage the rule applies to, `null` otherwise. */
|
|
720
|
-
assertTaskAttempts(node
|
|
720
|
+
assertTaskAttempts(node , run ) {
|
|
721
721
|
if (!this.taskAttemptCheck || node.skill !== 'do-execute-plan' || !node.mutatesSource) return null;
|
|
722
722
|
const answer = this.taskAttemptCheck(node, run);
|
|
723
723
|
if (!answer || !['none', 'complete', 'incomplete'].includes(answer.state)) {
|
|
@@ -735,17 +735,17 @@ class WorkflowOrchestrator {
|
|
|
735
735
|
* of a fresh run's) without writing anything. Same stop rules and reason vocabulary as `catchUp`;
|
|
736
736
|
* `stageId` is what `catchUp` reports as `caughtUpTo`, and `node` the program node it stops on.
|
|
737
737
|
* @returns {{reason: string, stageId: string|null, node: Object|null}} */
|
|
738
|
-
previewHandoff({ taskId, taskClass, candidateStageIds }
|
|
738
|
+
previewHandoff({ taskId, taskClass, candidateStageIds } ) {
|
|
739
739
|
const existing = this.readRun(taskId);
|
|
740
740
|
const run = existing
|
|
741
741
|
? { state: existing.state, cursor: existing.cursor, program: structuredClone(existing.program) }
|
|
742
742
|
: { state: 'RUNNING', cursor: 0, program: compileProgram(this.engine.resolveWorkflow(taskClass)) };
|
|
743
743
|
const candidates = new Set(candidateStageIds);
|
|
744
|
-
const stop = (reason
|
|
744
|
+
const stop = (reason , node = null) => ({ reason, stageId: reason === 'reached-candidate' ? node.id : null, node });
|
|
745
745
|
if (run.state === 'REJECTED') return stop('run-rejected');
|
|
746
|
-
const candidateNodes = run.program.filter((n
|
|
747
|
-
if (candidateNodes.length > 0 && candidateNodes.every((n
|
|
748
|
-
return stop(`already-completed:${candidateNodes.map((n
|
|
746
|
+
const candidateNodes = run.program.filter((n ) => candidates.has(n.id));
|
|
747
|
+
if (candidateNodes.length > 0 && candidateNodes.every((n ) => n.status === 'completed' || n.status === 'approved' || n.status === 'skipped')) {
|
|
748
|
+
return stop(`already-completed:${candidateNodes.map((n ) => n.id).join(',')}`);
|
|
749
749
|
}
|
|
750
750
|
for (;;) {
|
|
751
751
|
if (run.state === 'COMPLETED' || run.state === 'REJECTED') return stop(`run-${run.state.toLowerCase()}`);
|
|
@@ -775,7 +775,7 @@ class WorkflowOrchestrator {
|
|
|
775
775
|
* @returns {{status:'compacted'|'unchanged'|'partial'|'skipped'|'failed', reason?:string, moved?:Array,
|
|
776
776
|
* failed?:Array<{artifact:string, path:string, message:string}>}} `partial` carries `failed`, one entry per refused artifact
|
|
777
777
|
*/
|
|
778
|
-
function compactAfterHandoff({ taskId, projectRoot }
|
|
778
|
+
function compactAfterHandoff({ taskId, projectRoot } ) {
|
|
779
779
|
try {
|
|
780
780
|
const feature = resolveActiveFeature({ projectRoot, slug: taskId });
|
|
781
781
|
if (feature.error) return { status: 'skipped', reason: feature.message };
|
|
@@ -792,7 +792,7 @@ function compactAfterHandoff({ taskId, projectRoot }: any) {
|
|
|
792
792
|
}
|
|
793
793
|
if (result.finding) return { status: 'failed', reason: result.message };
|
|
794
794
|
return { status: result.status, moved: result.moved };
|
|
795
|
-
} catch (error
|
|
795
|
+
} catch (error ) {
|
|
796
796
|
return { status: 'failed', reason: error.message };
|
|
797
797
|
}
|
|
798
798
|
}
|
|
@@ -804,8 +804,8 @@ function compactAfterHandoff({ taskId, projectRoot }: any) {
|
|
|
804
804
|
function handleOrchestrateCommand({
|
|
805
805
|
action = 'status', taskId, taskClass, stage, gate, node, decision, note, reason, forced = false,
|
|
806
806
|
verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, slug = null, buildAdapters = null,
|
|
807
|
-
}
|
|
808
|
-
const { finishRuntime, usageError } = require('./cli-result.
|
|
807
|
+
} = {}) {
|
|
808
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
809
809
|
|
|
810
810
|
const root = repoRoot || REPO_ROOT;
|
|
811
811
|
// Same scope rule every other stateful command uses (evidenceRoot(o) in runtime-commands.js):
|
|
@@ -834,7 +834,7 @@ function handleOrchestrateCommand({
|
|
|
834
834
|
stateDir: path.join(runRoot, '.doflow', 'state', 'orchestration'),
|
|
835
835
|
featureSlug, buildAdapters,
|
|
836
836
|
});
|
|
837
|
-
orchestrator.readinessEvaluate = (node
|
|
837
|
+
orchestrator.readinessEvaluate = (node , run ) => {
|
|
838
838
|
// This closure only ever runs for a stage that is actually gated (evaluateReadiness only calls
|
|
839
839
|
// it for a mutating stage carrying a readiness template) — so a missing --task-class here is a
|
|
840
840
|
// caller mistake on THIS command, not a fact about the task's readiness. Reported before the
|
|
@@ -861,7 +861,7 @@ function handleOrchestrateCommand({
|
|
|
861
861
|
|
|
862
862
|
// FR-006. The ledger lives under the main worktree, so the plan and the store are found from any
|
|
863
863
|
// linked worktree; a run whose feature cannot be named has no ledger to check.
|
|
864
|
-
orchestrator.taskAttemptCheck = (node
|
|
864
|
+
orchestrator.taskAttemptCheck = (node , run ) => checkTaskAttemptCompletion({
|
|
865
865
|
slug: runSlugOf(run, runRoot) || featureSlug, cwd: state,
|
|
866
866
|
});
|
|
867
867
|
|
|
@@ -942,7 +942,7 @@ function handleOrchestrateCommand({
|
|
|
942
942
|
// MCP warnings go to stderr and notices to stdout, after everything else (IC-011).
|
|
943
943
|
for (const message of snapshot.mcpWarnings || []) (message.text.startsWith('[WARN]') ? console.error : console.log)(message.text);
|
|
944
944
|
return finishRuntime(0);
|
|
945
|
-
} catch (error
|
|
945
|
+
} catch (error ) {
|
|
946
946
|
// Transition refusals are findings, not crashes: out-of-order stages and unready gates are
|
|
947
947
|
// exactly what an operator asked about when they ran this.
|
|
948
948
|
console.error(`[ERROR] orchestrate: ${error.message}`);
|