@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
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
const fs = require('node:fs');
|
|
23
23
|
const path = require('node:path');
|
|
24
24
|
const crypto = require('node:crypto');
|
|
25
|
-
const { updateTaskState, readTaskState } = require('./task-state.
|
|
26
|
-
const { parsePlan } = require('./scaffold/artifacts.
|
|
27
|
-
const { EvidenceLedger } = require('./evidence-ledger.
|
|
28
|
-
const { projectRoot: lifecycleProjectRoot } = require('./lifecycle/root.
|
|
29
|
-
const { isSafeSlug } = require('./task-scope.
|
|
25
|
+
const { updateTaskState, readTaskState } = require('./task-state.js');
|
|
26
|
+
const { parsePlan } = require('./scaffold/artifacts.js');
|
|
27
|
+
const { EvidenceLedger } = require('./evidence-ledger.js');
|
|
28
|
+
const { projectRoot: lifecycleProjectRoot } = require('./lifecycle/root.js');
|
|
29
|
+
const { isSafeSlug } = require('./task-scope.js');
|
|
30
30
|
|
|
31
31
|
const SCHEMA_VERSION = 1;
|
|
32
32
|
const MODES = Object.freeze(['manual', 'sequential', 'subagent']);
|
|
@@ -37,8 +37,8 @@ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._ -]*$/;
|
|
|
37
37
|
|
|
38
38
|
/** A failure that is the caller's or the environment's, not a finding: exit 2. */
|
|
39
39
|
class TaskAttemptError extends Error {
|
|
40
|
-
|
|
41
|
-
constructor(code
|
|
40
|
+
|
|
41
|
+
constructor(code , message ) {
|
|
42
42
|
super(message);
|
|
43
43
|
this.name = 'TaskAttemptError';
|
|
44
44
|
this.code = code;
|
|
@@ -48,13 +48,13 @@ class TaskAttemptError extends Error {
|
|
|
48
48
|
/** Thrown inside the locked build to abandon a write without touching the store. */
|
|
49
49
|
class NoWrite extends Error {}
|
|
50
50
|
|
|
51
|
-
function storeFile(root
|
|
51
|
+
function storeFile(root , slug ) {
|
|
52
52
|
return path.join(root, '.doflow', 'state', 'task-attempts', `${slug}.json`);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function uniq(values
|
|
55
|
+
function uniq(values ) { return [...new Set(values)]; }
|
|
56
56
|
|
|
57
|
-
function assertSlug(slug
|
|
57
|
+
function assertSlug(slug ) {
|
|
58
58
|
if (!isSafeSlug(slug)) throw new TaskAttemptError('usage', `'${slug}' is not a valid feature slug`);
|
|
59
59
|
return slug;
|
|
60
60
|
}
|
|
@@ -63,7 +63,7 @@ function assertSlug(slug: any) {
|
|
|
63
63
|
|
|
64
64
|
/** The hash a claim records and `finish` compares: everything about the task's plan line except
|
|
65
65
|
* its checkbox, so ticking the box after a finish is not an edit of the task. */
|
|
66
|
-
function taskLineHash(task
|
|
66
|
+
function taskLineHash(task ) {
|
|
67
67
|
const material = JSON.stringify([
|
|
68
68
|
task.id, task.description, task.stories, task.owner, task.files, task.dependsOn, task.externalContract,
|
|
69
69
|
]);
|
|
@@ -74,27 +74,27 @@ function taskLineHash(task: any) {
|
|
|
74
74
|
* The plan's tasks, in plan order, with phase order taken from first appearance.
|
|
75
75
|
* @returns {{planPath: string, tasks: Array<{id: string, phase: string, phaseIndex: number, checked: boolean, hash: string}>}}
|
|
76
76
|
*/
|
|
77
|
-
function loadPlan({ roots, slug, fsImpl = fs }
|
|
77
|
+
function loadPlan({ roots, slug, fsImpl = fs } ) {
|
|
78
78
|
const relative = path.join('agent-docs', 'doflow', slug, 'plan.md');
|
|
79
|
-
const found = roots.map((root
|
|
79
|
+
const found = roots.map((root ) => path.join(root, relative)).find((file ) => fsImpl.existsSync(file));
|
|
80
80
|
if (!found) throw new TaskAttemptError('plan-unreadable', `no plan.md for feature '${slug}' (looked for ${relative} under ${roots.join(', ')})`);
|
|
81
81
|
let text;
|
|
82
|
-
try { text = fsImpl.readFileSync(found, 'utf8'); } catch (error
|
|
82
|
+
try { text = fsImpl.readFileSync(found, 'utf8'); } catch (error ) {
|
|
83
83
|
throw new TaskAttemptError('plan-unreadable', `cannot read ${found}: ${error.message}`);
|
|
84
84
|
}
|
|
85
85
|
const parsed = parsePlan(text);
|
|
86
86
|
if (parsed.tasks.length === 0) {
|
|
87
|
-
throw new TaskAttemptError('plan-malformed', `${found} holds no \`- [ ] <phase>.<n>\` tasks: ${parsed.gaps.map((g
|
|
87
|
+
throw new TaskAttemptError('plan-malformed', `${found} holds no \`- [ ] <phase>.<n>\` tasks: ${parsed.gaps.map((g ) => g.why).join('; ') || 'nothing to track'}`);
|
|
88
88
|
}
|
|
89
89
|
const seen = new Set();
|
|
90
90
|
for (const task of parsed.tasks) {
|
|
91
91
|
if (seen.has(task.id)) throw new TaskAttemptError('plan-malformed', `plan task id '${task.id}' appears more than once in ${found}`);
|
|
92
92
|
seen.add(task.id);
|
|
93
93
|
}
|
|
94
|
-
const phases = uniq(parsed.tasks.map((t
|
|
94
|
+
const phases = uniq(parsed.tasks.map((t ) => t.phase));
|
|
95
95
|
return {
|
|
96
96
|
planPath: relative.split(path.sep).join('/'),
|
|
97
|
-
tasks: parsed.tasks.map((t
|
|
97
|
+
tasks: parsed.tasks.map((t ) => ({
|
|
98
98
|
id: t.id, phase: t.phase, phaseIndex: phases.indexOf(t.phase), checked: t.done, hash: taskLineHash(t),
|
|
99
99
|
})),
|
|
100
100
|
};
|
|
@@ -102,19 +102,19 @@ function loadPlan({ roots, slug, fsImpl = fs }: any) {
|
|
|
102
102
|
|
|
103
103
|
// ── store ──────────────────────────────────────────────────────────────────────────────────────
|
|
104
104
|
|
|
105
|
-
function unreadable(file
|
|
105
|
+
function unreadable(file , why ) {
|
|
106
106
|
return new TaskAttemptError('store-unreadable', `${why} Refusing to read or write '${file}'; it was left unchanged.`);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/** The store as an object, or a fresh empty one when the file does not exist. Shape-checked: a file
|
|
110
110
|
* that parses but is not a ledger is as unusable as one that does not parse. */
|
|
111
|
-
function normalizeStore(data
|
|
111
|
+
function normalizeStore(data , file ) {
|
|
112
112
|
if (data === null || data === undefined) return { version: SCHEMA_VERSION, revision: 0, planPath: null, tasks: {} };
|
|
113
113
|
if (typeof data !== 'object' || Array.isArray(data) || data.version !== SCHEMA_VERSION
|
|
114
114
|
|| typeof data.tasks !== 'object' || data.tasks === null || Array.isArray(data.tasks)) {
|
|
115
115
|
throw unreadable(file, 'It is not a version-1 task-attempt ledger.');
|
|
116
116
|
}
|
|
117
|
-
for (const [id, entry] of (Object.entries(data.tasks)
|
|
117
|
+
for (const [id, entry] of (Object.entries(data.tasks) )) {
|
|
118
118
|
if (!entry || !Number.isInteger(entry.nextToken) || !Array.isArray(entry.attempts)) {
|
|
119
119
|
throw unreadable(file, `Task '${id}' has no readable attempt list.`);
|
|
120
120
|
}
|
|
@@ -122,10 +122,10 @@ function normalizeStore(data: any, file: any) {
|
|
|
122
122
|
return data;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function readStore(file
|
|
125
|
+
function readStore(file , fsImpl ) {
|
|
126
126
|
try {
|
|
127
127
|
return normalizeStore(readTaskState(fsImpl, file), file);
|
|
128
|
-
} catch (error
|
|
128
|
+
} catch (error ) {
|
|
129
129
|
if (error instanceof TaskAttemptError) throw error;
|
|
130
130
|
throw unreadable(file, error.message);
|
|
131
131
|
}
|
|
@@ -133,12 +133,12 @@ function readStore(file: any, fsImpl: any) {
|
|
|
133
133
|
|
|
134
134
|
/** One locked read-decide-write. `apply(store)` returns `{result, write}`; with `write` false the
|
|
135
135
|
* store is not touched, which is how a refusal or a stale token changes nothing. */
|
|
136
|
-
function mutate({ file, fsImpl, apply }
|
|
137
|
-
let outcome
|
|
136
|
+
function mutate({ file, fsImpl, apply } ) {
|
|
137
|
+
let outcome = null;
|
|
138
138
|
try {
|
|
139
139
|
updateTaskState({
|
|
140
140
|
fsImpl, file,
|
|
141
|
-
build: (disk
|
|
141
|
+
build: (disk ) => {
|
|
142
142
|
const store = normalizeStore(disk, file);
|
|
143
143
|
store.revision = (Number.isInteger(store.revision) ? store.revision : 0) + 1;
|
|
144
144
|
outcome = apply(store);
|
|
@@ -146,7 +146,7 @@ function mutate({ file, fsImpl, apply }: any) {
|
|
|
146
146
|
return store;
|
|
147
147
|
},
|
|
148
148
|
});
|
|
149
|
-
} catch (error
|
|
149
|
+
} catch (error ) {
|
|
150
150
|
if (error instanceof NoWrite) return outcome.result;
|
|
151
151
|
if (error instanceof TaskAttemptError) throw error;
|
|
152
152
|
throw unreadable(file, error.message);
|
|
@@ -154,24 +154,24 @@ function mutate({ file, fsImpl, apply }: any) {
|
|
|
154
154
|
return outcome.result;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function entryFor(store
|
|
157
|
+
function entryFor(store , taskId ) {
|
|
158
158
|
if (!store.tasks[taskId]) store.tasks[taskId] = { nextToken: 1, attempts: [] };
|
|
159
159
|
return store.tasks[taskId];
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
const activeAttempt = (entry
|
|
163
|
-
const doneAttempt = (entry
|
|
162
|
+
const activeAttempt = (entry ) => (entry ? entry.attempts.find((a ) => a.state === 'active') || null : null);
|
|
163
|
+
const doneAttempt = (entry ) => (entry ? entry.attempts.find((a ) => a.state === 'finished' && a.result === 'COMPLETED') || null : null);
|
|
164
164
|
|
|
165
|
-
function latestFinished(entry
|
|
166
|
-
const finished = entry ? entry.attempts.filter((a
|
|
167
|
-
return finished.length ? finished.reduce((a
|
|
165
|
+
function latestFinished(entry ) {
|
|
166
|
+
const finished = entry ? entry.attempts.filter((a ) => a.state === 'finished') : [];
|
|
167
|
+
return finished.length ? finished.reduce((a , b ) => (b.token > a.token ? b : a)) : null;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// ── evidence ───────────────────────────────────────────────────────────────────────────────────
|
|
171
171
|
|
|
172
172
|
/** Evidence ids recorded in any task file under the evidence store, to tell "belongs to another task"
|
|
173
173
|
* from "does not exist". Reads only; a file that cannot be read simply does not match. */
|
|
174
|
-
function evidenceBelongsElsewhere({ roots, slug, evidenceId, fsImpl }
|
|
174
|
+
function evidenceBelongsElsewhere({ roots, slug, evidenceId, fsImpl } ) {
|
|
175
175
|
for (const root of roots) {
|
|
176
176
|
const base = path.join(root, '.doflow', 'state', 'evidence');
|
|
177
177
|
for (const dir of [base, path.join(base, slug)]) {
|
|
@@ -181,7 +181,7 @@ function evidenceBelongsElsewhere({ roots, slug, evidenceId, fsImpl }: any) {
|
|
|
181
181
|
if (!name.endsWith('.json')) continue;
|
|
182
182
|
try {
|
|
183
183
|
const data = JSON.parse(fsImpl.readFileSync(path.join(dir, name), 'utf8'));
|
|
184
|
-
if (Array.isArray(data.evidence) && data.evidence.some((item
|
|
184
|
+
if (Array.isArray(data.evidence) && data.evidence.some((item ) => item && item.id === evidenceId)) return true;
|
|
185
185
|
} catch { /* unreadable files do not match */ }
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -190,7 +190,7 @@ function evidenceBelongsElsewhere({ roots, slug, evidenceId, fsImpl }: any) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/** The refusal reason for `evidenceId` as proof of a COMPLETED attempt, or null when it qualifies (FR-004). */
|
|
193
|
-
function evidenceRefusal({ evidenceId, slug, taskId, startedAt, roots, fsImpl }
|
|
193
|
+
function evidenceRefusal({ evidenceId, slug, taskId, startedAt, roots, fsImpl } ) {
|
|
194
194
|
if (!evidenceId) return 'evidence-required';
|
|
195
195
|
const allowed = [slug, taskId];
|
|
196
196
|
let item = null;
|
|
@@ -216,25 +216,25 @@ function evidenceRefusal({ evidenceId, slug, taskId, startedAt, roots, fsImpl }:
|
|
|
216
216
|
|
|
217
217
|
// ── actions ────────────────────────────────────────────────────────────────────────────────────
|
|
218
218
|
|
|
219
|
-
function rootsFor({ cwd, lifecycleRoot }
|
|
219
|
+
function rootsFor({ cwd, lifecycleRoot } ) {
|
|
220
220
|
return uniq([path.resolve(cwd), lifecycleRoot]);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
function claim({ file, fsImpl, roots, slug, task, owner, mode, now = new Date() }
|
|
223
|
+
function claim({ file, fsImpl, roots, slug, task, owner, mode, now = new Date() } ) {
|
|
224
224
|
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
225
225
|
if (!owner || !SAFE_NAME.test(owner)) throw new TaskAttemptError('usage', '--owner is required: the name of the agent or person taking the task');
|
|
226
226
|
if (!MODES.includes(mode)) throw new TaskAttemptError('usage', `--mode is required: one of ${MODES.join(', ')}`);
|
|
227
227
|
const plan = loadPlan({ roots, slug, fsImpl });
|
|
228
228
|
return mutate({
|
|
229
229
|
file, fsImpl,
|
|
230
|
-
apply: (store
|
|
231
|
-
const refuse = (reason
|
|
232
|
-
const target = plan.tasks.find((t
|
|
230
|
+
apply: (store ) => {
|
|
231
|
+
const refuse = (reason , extra = {}) => ({ write: false, result: { disposition: 'refused', taskId: task, reason, ...extra } });
|
|
232
|
+
const target = plan.tasks.find((t ) => t.id === task);
|
|
233
233
|
if (!target) return refuse('unknown-task');
|
|
234
234
|
if (doneAttempt(store.tasks[task])) return refuse('already-done');
|
|
235
235
|
const active = activeAttempt(store.tasks[task]);
|
|
236
236
|
if (active) return refuse('active-attempt', { activeToken: active.token, owner: active.owner, startedAt: active.startedAt });
|
|
237
|
-
const unmet = plan.tasks.filter((t
|
|
237
|
+
const unmet = plan.tasks.filter((t ) => t.phaseIndex < target.phaseIndex && !doneAttempt(store.tasks[t.id])).map((t ) => t.id);
|
|
238
238
|
if (unmet.length) return refuse('unmet-phase', { unmet });
|
|
239
239
|
const entry = entryFor(store, task);
|
|
240
240
|
const token = entry.nextToken;
|
|
@@ -249,23 +249,23 @@ function claim({ file, fsImpl, roots, slug, task, owner, mode, now = new Date()
|
|
|
249
249
|
});
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
function stale(taskId
|
|
252
|
+
function stale(taskId , token , reason ) {
|
|
253
253
|
return { write: false, result: { disposition: 'stale', taskId, token, reason } };
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId }
|
|
256
|
+
function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId } ) {
|
|
257
257
|
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
258
258
|
if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token claim returned');
|
|
259
259
|
if (!RESULTS.includes(result)) throw new TaskAttemptError('usage', `--result is required: one of ${RESULTS.join(', ')}`);
|
|
260
260
|
return mutate({
|
|
261
261
|
file, fsImpl,
|
|
262
|
-
apply: (store
|
|
262
|
+
apply: (store ) => {
|
|
263
263
|
const active = activeAttempt(store.tasks[task]);
|
|
264
264
|
if (!active) return stale(task, token, 'no-active-attempt');
|
|
265
265
|
if (active.token !== token) return stale(task, token, 'token-mismatch');
|
|
266
|
-
const refuse = (reason
|
|
266
|
+
const refuse = (reason ) => ({ write: false, result: { disposition: 'refused', taskId: task, token, reason } });
|
|
267
267
|
const plan = loadPlan({ roots, slug, fsImpl });
|
|
268
|
-
const line = plan.tasks.find((t
|
|
268
|
+
const line = plan.tasks.find((t ) => t.id === task);
|
|
269
269
|
if (!line) return refuse('unknown-task');
|
|
270
270
|
if (line.hash !== active.taskLineHash) return refuse('plan-changed');
|
|
271
271
|
if (result === 'COMPLETED') {
|
|
@@ -280,13 +280,13 @@ function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId }:
|
|
|
280
280
|
});
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
function release({ file, fsImpl, task, token, reason }
|
|
283
|
+
function release({ file, fsImpl, task, token, reason } ) {
|
|
284
284
|
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
285
285
|
if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token claim returned');
|
|
286
286
|
if (!reason || !String(reason).trim()) throw new TaskAttemptError('usage', '--reason is required: why the attempt is being released');
|
|
287
287
|
return mutate({
|
|
288
288
|
file, fsImpl,
|
|
289
|
-
apply: (store
|
|
289
|
+
apply: (store ) => {
|
|
290
290
|
const active = activeAttempt(store.tasks[task]);
|
|
291
291
|
if (!active) return stale(task, token, 'no-active-attempt');
|
|
292
292
|
if (active.token !== token) return stale(task, token, 'token-mismatch');
|
|
@@ -298,10 +298,10 @@ function release({ file, fsImpl, task, token, reason }: any) {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/** Completion read from the ledger and the plan, never from checkboxes (FR-005). */
|
|
301
|
-
function status({ file, fsImpl, roots, slug }
|
|
301
|
+
function status({ file, fsImpl, roots, slug } ) {
|
|
302
302
|
const plan = loadPlan({ roots, slug, fsImpl });
|
|
303
303
|
const store = readStore(file, fsImpl);
|
|
304
|
-
const tasks = plan.tasks.map((t
|
|
304
|
+
const tasks = plan.tasks.map((t ) => {
|
|
305
305
|
const entry = store.tasks[t.id];
|
|
306
306
|
const active = activeAttempt(entry);
|
|
307
307
|
const done = doneAttempt(entry);
|
|
@@ -310,7 +310,7 @@ function status({ file, fsImpl, roots, slug }: any) {
|
|
|
310
310
|
if (done) derived = 'done';
|
|
311
311
|
else if (active) derived = 'running';
|
|
312
312
|
else if (last && last.result === 'BLOCKED') derived = 'blocked';
|
|
313
|
-
else if (plan.tasks.every((o
|
|
313
|
+
else if (plan.tasks.every((o ) => o.phaseIndex >= t.phaseIndex || doneAttempt(store.tasks[o.id]))) derived = 'ready';
|
|
314
314
|
else derived = 'pending';
|
|
315
315
|
return {
|
|
316
316
|
id: t.id, phase: t.phase, derived,
|
|
@@ -320,10 +320,10 @@ function status({ file, fsImpl, roots, slug }: any) {
|
|
|
320
320
|
};
|
|
321
321
|
});
|
|
322
322
|
return {
|
|
323
|
-
complete: tasks.length > 0 && tasks.every((t
|
|
323
|
+
complete: tasks.length > 0 && tasks.every((t ) => t.derived === 'done'),
|
|
324
324
|
revision: store.revision,
|
|
325
325
|
tasks,
|
|
326
|
-
unclaimedChecked: plan.tasks.filter((t
|
|
326
|
+
unclaimedChecked: plan.tasks.filter((t ) => t.checked && !doneAttempt(store.tasks[t.id])).map((t ) => t.id),
|
|
327
327
|
};
|
|
328
328
|
}
|
|
329
329
|
|
|
@@ -333,26 +333,26 @@ function status({ file, fsImpl, roots, slug }: any) {
|
|
|
333
333
|
* otherwise. A store or plan that cannot be read throws, so the gate refuses rather than guessing.
|
|
334
334
|
* @returns {{state: 'none'|'complete'|'incomplete', slug: string|null, incomplete: Array<string>}}
|
|
335
335
|
*/
|
|
336
|
-
function checkCompletion({ slug, cwd, fsImpl = fs }
|
|
336
|
+
function checkCompletion({ slug, cwd, fsImpl = fs } ) {
|
|
337
337
|
if (!isSafeSlug(slug)) return { state: 'none', slug: null, incomplete: [] };
|
|
338
338
|
const lifecycleRoot = lifecycleProjectRoot(cwd);
|
|
339
339
|
const file = storeFile(lifecycleRoot, slug);
|
|
340
340
|
if (!fsImpl.existsSync(file)) return { state: 'none', slug, incomplete: [] };
|
|
341
341
|
const answer = status({ file, fsImpl, roots: rootsFor({ cwd, lifecycleRoot }), slug });
|
|
342
|
-
const incomplete = answer.tasks.filter((t
|
|
342
|
+
const incomplete = answer.tasks.filter((t ) => t.derived !== 'done').map((t ) => t.id);
|
|
343
343
|
return { state: answer.complete ? 'complete' : 'incomplete', slug, incomplete };
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
// ── CLI ────────────────────────────────────────────────────────────────────────────────────────
|
|
347
347
|
|
|
348
|
-
function failure(code
|
|
349
|
-
const { finishRuntime } = require('./cli-result.
|
|
348
|
+
function failure(code , message , json ) {
|
|
349
|
+
const { finishRuntime } = require('./cli-result.js');
|
|
350
350
|
if (json) console.log(JSON.stringify({ ok: false, error: code, message }, null, 2));
|
|
351
351
|
else console.error(`doflow task-attempt: ${message}`);
|
|
352
352
|
return finishRuntime(2);
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
function describe(result
|
|
355
|
+
function describe(result ) {
|
|
356
356
|
const parts = [`${result.disposition}`];
|
|
357
357
|
if (result.taskId) parts.push(`task ${result.taskId}`);
|
|
358
358
|
if (result.token !== undefined) parts.push(`token ${result.token}`);
|
|
@@ -370,8 +370,8 @@ function describe(result: any) {
|
|
|
370
370
|
function handleTaskAttemptCommand({
|
|
371
371
|
action = 'status', slug = null, task, owner, mode, token, result, evidenceId, reason, json = false,
|
|
372
372
|
cwd = process.cwd(), resolveSlug = null, fsImpl = fs, now = new Date(),
|
|
373
|
-
}
|
|
374
|
-
const { finishRuntime } = require('./cli-result.
|
|
373
|
+
} = {}) {
|
|
374
|
+
const { finishRuntime } = require('./cli-result.js');
|
|
375
375
|
try {
|
|
376
376
|
if (!ACTIONS.includes(action)) throw new TaskAttemptError('usage', `unknown action '${action}'; valid: ${ACTIONS.join(' | ')}`);
|
|
377
377
|
const feature = slug || (resolveSlug ? resolveSlug(cwd) : null);
|
|
@@ -403,7 +403,7 @@ function handleTaskAttemptCommand({
|
|
|
403
403
|
if (answer.unclaimedChecked.length) console.log(`Checked in plan.md with no COMPLETED attempt: ${answer.unclaimedChecked.join(', ')}`);
|
|
404
404
|
} else (ok ? console.log : console.error)(describe(answer));
|
|
405
405
|
return finishRuntime(ok ? 0 : 1);
|
|
406
|
-
} catch (error
|
|
406
|
+
} catch (error ) {
|
|
407
407
|
if (error instanceof TaskAttemptError) return failure(error.code, error.message, json);
|
|
408
408
|
throw error;
|
|
409
409
|
}
|
|
@@ -411,5 +411,5 @@ function handleTaskAttemptCommand({
|
|
|
411
411
|
|
|
412
412
|
module.exports = {
|
|
413
413
|
handleTaskAttemptCommand, checkCompletion, claim, finish, release, status, loadPlan, taskLineHash,
|
|
414
|
-
storeFile, TaskAttemptError, MODES, RESULTS, ACTIONS,
|
|
414
|
+
storeFile, TaskAttemptError, MODES, RESULTS, ACTIONS, activeAttempt,
|
|
415
415
|
};
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const { TextDecoder } = require('node:util');
|
|
8
|
-
const { WorkflowEngine, CALLER_ROLES } = require('./workflow-engine.
|
|
9
|
-
const { finishRuntime } = require('./cli-result.
|
|
10
|
-
const { REPO_ROOT } = require('../helper/repo-root.
|
|
11
|
-
const { loadAdviceRegistry } = require('./advice/registry.
|
|
12
|
-
const { createAdviceClient } = require('./advice/client.
|
|
13
|
-
const { materializeKind } = require('./advice/kinds.
|
|
14
|
-
const { appendOutcome, findPick } = require('./advice/record.
|
|
15
|
-
const { strengthWords } = require('./advice/strength.
|
|
16
|
-
const { loadStorageRegistry } = require('./storage/registry.
|
|
17
|
-
const { resolveRoots } = require('./storage/roots.
|
|
8
|
+
const { WorkflowEngine, CALLER_ROLES } = require('./workflow-engine.js');
|
|
9
|
+
const { finishRuntime } = require('./cli-result.js');
|
|
10
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
11
|
+
const { loadAdviceRegistry } = require('./advice/registry.js');
|
|
12
|
+
const { createAdviceClient } = require('./advice/client.js');
|
|
13
|
+
const { materializeKind } = require('./advice/kinds.js');
|
|
14
|
+
const { appendOutcome, findPick } = require('./advice/record.js');
|
|
15
|
+
const { strengthWords } = require('./advice/strength.js');
|
|
16
|
+
const { loadStorageRegistry } = require('./storage/registry.js');
|
|
17
|
+
const { resolveRoots } = require('./storage/roots.js');
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -79,7 +79,7 @@ const NEAR_MISS_KEYS = ['class', 'taskType', 'type', 'proposedClass', 'workflow'
|
|
|
79
79
|
* silent skip of the fit check. */
|
|
80
80
|
const CALLER_NEAR_MISS_KEYS = ['caller', 'callerSkill', 'skill', 'skillId', 'invokedBy', 'calledBy'];
|
|
81
81
|
|
|
82
|
-
function isPlainObject(value
|
|
82
|
+
function isPlainObject(value ) {
|
|
83
83
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -90,7 +90,7 @@ function isPlainObject(value: any) {
|
|
|
90
90
|
* a silent match. Trimming here would be a small coercion that hides a real formatting bug in the
|
|
91
91
|
* caller's output.
|
|
92
92
|
*/
|
|
93
|
-
function readProposal(proposal
|
|
93
|
+
function readProposal(proposal ) {
|
|
94
94
|
if (proposal === null || proposal === undefined) {
|
|
95
95
|
return {
|
|
96
96
|
taskClass: null, rationale: null, proposedBy: null, callingSkill: null,
|
|
@@ -138,10 +138,10 @@ function readProposal(proposal: any) {
|
|
|
138
138
|
* every `do-*` skill. That is acceptable for a hint nobody acts on; it is why the messages present
|
|
139
139
|
* the result as "Did you mean" rather than as a shortlist.
|
|
140
140
|
*/
|
|
141
|
-
function suggestFrom(proposed
|
|
141
|
+
function suggestFrom(proposed , candidates ) {
|
|
142
142
|
if (typeof proposed !== 'string' || proposed.trim() === '') return [];
|
|
143
143
|
const needle = proposed.trim().toLowerCase();
|
|
144
|
-
return candidates.filter((candidate
|
|
144
|
+
return candidates.filter((candidate ) => {
|
|
145
145
|
const other = candidate.toLowerCase();
|
|
146
146
|
return other === needle || other.includes(needle) || needle.includes(other);
|
|
147
147
|
});
|
|
@@ -163,8 +163,8 @@ function suggestFrom(proposed: any, candidates: any) {
|
|
|
163
163
|
* hostedStageIds: Array<string>, hostingClasses: Array<{taskClass: string, stageIds: Array<string>}>,
|
|
164
164
|
* callerSuggestions: Array<string>}} frozen
|
|
165
165
|
*/
|
|
166
|
-
function judgeFit(engine
|
|
167
|
-
const unevaluated = (reason
|
|
166
|
+
function judgeFit(engine , taskClass , callingSkill , nearMissCallerKeys ) {
|
|
167
|
+
const unevaluated = (reason , extra = {}) => Object.freeze({
|
|
168
168
|
state: FIT_STATES.NOT_EVALUATED,
|
|
169
169
|
reason,
|
|
170
170
|
callingSkill,
|
|
@@ -194,7 +194,7 @@ function judgeFit(engine: any, taskClass: string, callingSkill: any, nearMissCal
|
|
|
194
194
|
// Derived from the stage lists rather than declared, so it cannot drift from them. On HOSTED it
|
|
195
195
|
// includes the accepted class: it is the caller's complete map, not a list of alternatives.
|
|
196
196
|
const hostingClasses = engine.classesHosting(callingSkill);
|
|
197
|
-
const decided = (state
|
|
197
|
+
const decided = (state , reason , hostedStageIds = []) => Object.freeze({
|
|
198
198
|
state, reason, callingSkill, callerRole, hostedStageIds, hostingClasses, callerSuggestions: [],
|
|
199
199
|
});
|
|
200
200
|
|
|
@@ -212,9 +212,9 @@ function judgeFit(engine: any, taskClass: string, callingSkill: any, nearMissCal
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/** `bug (root-cause), refactor (architecture-mapping)` — the caller's hosting map, as prose. */
|
|
215
|
-
function renderHostingClasses(hostingClasses
|
|
215
|
+
function renderHostingClasses(hostingClasses ) {
|
|
216
216
|
return hostingClasses
|
|
217
|
-
.map(({ taskClass, stageIds }
|
|
217
|
+
.map(({ taskClass, stageIds } ) => `${taskClass} (${stageIds.join(', ')})`)
|
|
218
218
|
.join(', ');
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -223,7 +223,7 @@ function renderHostingClasses(hostingClasses: any) {
|
|
|
223
223
|
* when fit was not evaluated: an operator must be able to read the blind spot off the message
|
|
224
224
|
* without inspecting the JSON.
|
|
225
225
|
*/
|
|
226
|
-
function describeAcceptedFit(fit
|
|
226
|
+
function describeAcceptedFit(fit , nearMissCallerKeys ) {
|
|
227
227
|
if (fit.state === FIT_STATES.HOSTED) {
|
|
228
228
|
return ` It hosts '${fit.callingSkill}' at stage(s): ${fit.hostedStageIds.join(', ')}.`;
|
|
229
229
|
}
|
|
@@ -233,7 +233,7 @@ function describeAcceptedFit(fit: any, nearMissCallerKeys: any) {
|
|
|
233
233
|
}
|
|
234
234
|
if (fit.reason === FIT_REASONS.CALLER_KEY_NEAR_MISS) {
|
|
235
235
|
return ' Fit was NOT evaluated: the proposal carries '
|
|
236
|
-
+ `${nearMissCallerKeys.map((key
|
|
236
|
+
+ `${nearMissCallerKeys.map((key ) => `'${key}'`).join(', ')}; the calling skill must be given `
|
|
237
237
|
+ 'as `callingSkill`. Membership is all this decision establishes.';
|
|
238
238
|
}
|
|
239
239
|
return ' Fit was NOT evaluated: no calling skill was named, so this decision establishes that the '
|
|
@@ -245,7 +245,7 @@ function describeAcceptedFit(fit: any, nearMissCallerKeys: any) {
|
|
|
245
245
|
* The message for a rejection the fit check produced. Each one names the remedy: a rejection nobody
|
|
246
246
|
* can act on is a stop rather than a fix.
|
|
247
247
|
*/
|
|
248
|
-
function describeFitRejection(fit
|
|
248
|
+
function describeFitRejection(fit , taskClass , workflow , engine ) {
|
|
249
249
|
if (fit.reason === REJECTION_REASONS.UNKNOWN_CALLER) {
|
|
250
250
|
const hint = fit.callerSuggestions.length > 0
|
|
251
251
|
? ` Did you mean: ${fit.callerSuggestions.join(', ')}?`
|
|
@@ -272,14 +272,14 @@ function describeFitRejection(fit: any, taskClass: any, workflow: any, engine: a
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
class TaskClassifier {
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
|
|
276
|
+
|
|
277
277
|
/**
|
|
278
278
|
* @param {Object} [options]
|
|
279
279
|
* @param {WorkflowEngine} [options.engine] Pre-built engine; otherwise one is built from `options`.
|
|
280
280
|
* @param {Function} [options.now] Clock returning an ISO timestamp, for deterministic tests.
|
|
281
281
|
*/
|
|
282
|
-
constructor(options
|
|
282
|
+
constructor(options = {}) {
|
|
283
283
|
this.engine = options.engine || new WorkflowEngine(options);
|
|
284
284
|
this.now = options.now || (() => new Date().toISOString());
|
|
285
285
|
}
|
|
@@ -306,7 +306,7 @@ class TaskClassifier {
|
|
|
306
306
|
* skill can occupy — including `NOT_EVALUATED` when no caller was named, since an unchecked fit
|
|
307
307
|
* must never read as a checked one.
|
|
308
308
|
*/
|
|
309
|
-
classify(proposal
|
|
309
|
+
classify(proposal , context = {}) {
|
|
310
310
|
const validClasses = this.listClasses();
|
|
311
311
|
const parsed = readProposal(proposal);
|
|
312
312
|
const base = {
|
|
@@ -411,7 +411,7 @@ class TaskClassifier {
|
|
|
411
411
|
* @param {Object} [context]
|
|
412
412
|
* @returns {Object} the resolved workflow
|
|
413
413
|
*/
|
|
414
|
-
resolve(proposal
|
|
414
|
+
resolve(proposal , context = {}) {
|
|
415
415
|
const decision = this.classify(proposal, context);
|
|
416
416
|
if (decision.outcome !== CLASSIFICATION_OUTCOMES.ACCEPTED) {
|
|
417
417
|
throw new Error(decision.message);
|
|
@@ -438,7 +438,7 @@ class TaskClassifier {
|
|
|
438
438
|
* @param {boolean} [options.json=false]
|
|
439
439
|
* @returns {number|Promise<number>} exit code; advisory calls are asynchronous
|
|
440
440
|
*/
|
|
441
|
-
function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill, request, requestFile, taskId, projectRoot, json = false }
|
|
441
|
+
function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill, request, requestFile, taskId, projectRoot, json = false } = {}) {
|
|
442
442
|
const classifier = new TaskClassifier({ repoRoot: REPO_ROOT });
|
|
443
443
|
const decision = classifier.classify({ taskClass, rationale, proposedBy, callingSkill });
|
|
444
444
|
const exitCode = decision.outcome === CLASSIFICATION_OUTCOMES.ACCEPTED
|
|
@@ -459,7 +459,7 @@ function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill,
|
|
|
459
459
|
try {
|
|
460
460
|
state = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(requestFile));
|
|
461
461
|
if (!state.length) throw new Error('file is empty');
|
|
462
|
-
} catch (error
|
|
462
|
+
} catch (error ) {
|
|
463
463
|
fileError = error;
|
|
464
464
|
}
|
|
465
465
|
}
|
|
@@ -471,7 +471,7 @@ function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill,
|
|
|
471
471
|
const advice = loadAdviceRegistry({ repoRoot: REPO_ROOT });
|
|
472
472
|
const storage = loadStorageRegistry({ repoRoot: REPO_ROOT });
|
|
473
473
|
const roots = resolveRoots({ registry: storage, projectRoot: path.resolve(projectRoot || process.cwd()) });
|
|
474
|
-
const source = (name
|
|
474
|
+
const source = (name ) => JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'core/registry', name), 'utf8'));
|
|
475
475
|
const sources = { workflows: source('workflows.json') };
|
|
476
476
|
const built = materializeKind('task-class', { adviceRegistry: advice.ok ? advice.registry : null, sources });
|
|
477
477
|
const client = createAdviceClient({ adviceRegistry: advice.ok ? advice.registry : null, roots, sources });
|
|
@@ -498,12 +498,12 @@ function handleClassifyCommand({ taskClass, rationale, proposedBy, callingSkill,
|
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
function emptySecondOpinion(status
|
|
501
|
+
function emptySecondOpinion(status , reason ) {
|
|
502
502
|
return { status, reason, pick: null, strength: null, between: null, agrees: null, proposed: null,
|
|
503
503
|
pickId: null, probabilities: null, confidence: null, model: null };
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
function secondOpinion(result
|
|
506
|
+
function secondOpinion(result , proposed ) {
|
|
507
507
|
const answer = result.answers?.[0];
|
|
508
508
|
if (result.status !== 'answered' || !answer) return emptySecondOpinion(result.status, result.reason);
|
|
509
509
|
return {
|
|
@@ -513,7 +513,7 @@ function secondOpinion(result: any, proposed: any) {
|
|
|
513
513
|
};
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
function printClassifyDecision(decision
|
|
516
|
+
function printClassifyDecision(decision , opinion ) {
|
|
517
517
|
console.log(`\nDoFlow Task Classification [${decision.outcome}]:`);
|
|
518
518
|
console.log('═'.repeat(78));
|
|
519
519
|
console.log(decision.message);
|
|
@@ -527,7 +527,7 @@ function printClassifyDecision(decision: any, opinion: any) {
|
|
|
527
527
|
}
|
|
528
528
|
if (decision.fit.state === 'NOT_HOSTED' && decision.fit.hostingClasses.length > 0) {
|
|
529
529
|
console.log(`Classes that host ${decision.fit.callingSkill}: `
|
|
530
|
-
+ decision.fit.hostingClasses.map((h
|
|
530
|
+
+ decision.fit.hostingClasses.map((h ) => `${h.taskClass} (${h.stageIds.join(', ')})`).join(', '));
|
|
531
531
|
}
|
|
532
532
|
if (decision.workflow) {
|
|
533
533
|
console.log('─'.repeat(78));
|