@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.4
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/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/registry/storage.json +87 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +60 -60
- package/src/adapters/claude/{index.ts → index.js} +48 -48
- 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} +23 -23
- package/src/adapters/codex/{index.ts → index.js} +98 -98
- 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} +70 -70
- package/src/adapters/gemini/{hooks.ts → hooks.js} +24 -24
- package/src/adapters/gemini/{index.ts → index.js} +41 -41
- 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} +34 -34
- 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/{index.ts → index.js} +30 -30
- 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} +24 -24
- 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} +19 -19
- package/src/cli/{index.ts → index.js} +26 -26
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +48 -48
- 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/{index.ts → index.js} +140 -140
- 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} +60 -60
- 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/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +30 -30
- package/src/runtime/{cli-result.ts → cli-result.js} +4 -4
- package/src/runtime/{cli.ts → cli.js} +37 -37
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/{context-pack.ts → context-pack.js} +20 -20
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +25 -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} +15 -15
- 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} +35 -35
- 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} +40 -40
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +19 -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/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} +61 -61
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +13 -13
- package/src/runtime/{task-state.ts → task-state.js} +15 -15
- 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
|
@@ -9,30 +9,30 @@
|
|
|
9
9
|
* by the service and printed here, never thrown, so it cannot reach failure capture (NFR-005).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const { finishRuntime, usageError } = require('../cli-result.
|
|
13
|
-
const { projectRoot } = require('./root.
|
|
14
|
-
const followup = require('./followup.
|
|
15
|
-
const { StoreUnsafeError, StoreLockedError } = require('./event-store.
|
|
16
|
-
const { prepareStore } = require('./store-upkeep.
|
|
17
|
-
const { printSafe } = require('../mask.
|
|
18
|
-
const { buildOverview, initFeature, featureStatus } = require('./overview.
|
|
19
|
-
const { releaseFeatures, recordMerged } = require('./release.
|
|
20
|
-
const goal = require('./goal.
|
|
12
|
+
const { finishRuntime, usageError } = require('../cli-result.js');
|
|
13
|
+
const { projectRoot } = require('./root.js');
|
|
14
|
+
const followup = require('./followup.js');
|
|
15
|
+
const { StoreUnsafeError, StoreLockedError } = require('./event-store.js');
|
|
16
|
+
const { prepareStore } = require('./store-upkeep.js');
|
|
17
|
+
const { printSafe } = require('../mask.js');
|
|
18
|
+
const { buildOverview, initFeature, featureStatus } = require('./overview.js');
|
|
19
|
+
const { releaseFeatures, recordMerged } = require('./release.js');
|
|
20
|
+
const goal = require('./goal.js');
|
|
21
21
|
|
|
22
22
|
const FOLLOWUP_ACTIONS = ['add', 'list', 'take', 'settle', 'promote', 'report'];
|
|
23
23
|
const LIFECYCLE_ACTIONS = ['overview', 'init', 'release', 'status', 'merged'];
|
|
24
24
|
const GOAL_ACTIONS = ['add', 'item', 'check', 'link', 'list', 'done'];
|
|
25
25
|
/** The flags each goal action takes, besides --goal, --channel and --json; any other one given is a usage error. */
|
|
26
|
-
const GOAL_FLAGS
|
|
26
|
+
const GOAL_FLAGS = {
|
|
27
27
|
add: ['statement', 'item'], item: ['text'], check: ['item', 'evidence', 'unmet'],
|
|
28
28
|
link: ['slug', 'replace'], list: [], done: ['reason'],
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
function sourceText(source
|
|
31
|
+
function sourceText(source ) {
|
|
32
32
|
return [source.kind, source.feature, source.taskClass, source.taskId, source.release, source.stage, source.ref].filter(Boolean).join(' ');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function followupLines(result
|
|
35
|
+
function followupLines(result ) {
|
|
36
36
|
const lines = [];
|
|
37
37
|
switch (result.action) {
|
|
38
38
|
case 'add': for (const c of result.created) lines.push(`added ${c.id} (${sourceText(c.source)}): ${c.statement}`); break;
|
|
@@ -52,17 +52,17 @@ function followupLines(result: any) {
|
|
|
52
52
|
return lines;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function overviewLines(r
|
|
55
|
+
function overviewLines(r ) {
|
|
56
56
|
const lines = [`${r.mode} overview: ${r.followups.open} open follow-up${r.followups.open === 1 ? '' : 's'}, ${r.followups.shown} shown (release mode ${r.releaseMode}${r.integrationRef ? `, integration ref ${r.integrationRef}` : ''})`];
|
|
57
57
|
for (const i of r.followups.items) lines.push(` ${i.id} (${sourceText(i.source)}, ${i.added})${i.promoted ? ` promoted to ${i.intent}` : ''}${i.pending === false ? ' settled this run' : ''} ${i.statement}`);
|
|
58
58
|
if (Array.isArray(r.failures)) {
|
|
59
59
|
lines.push(`failures: ${r.failures.length} new or regressed`);
|
|
60
60
|
for (const f of r.failures) lines.push(` ${f.fp} ${f.status} x${f.count} ${f.command} ${f.kind}${f.message ? `: ${f.message}` : ''} (last seen ${f.lastSeen}, ${f.lastVersion})`);
|
|
61
61
|
}
|
|
62
|
-
for (const g of r.goals) lines.push(`goal ${g.goal}: ${g.items.met}/${g.items.total} items met${g.proposeDone ? ' (propose done)' : ''}`, ...g.nudges.map((n
|
|
62
|
+
for (const g of r.goals) lines.push(`goal ${g.goal}: ${g.items.met}/${g.items.total} items met${g.proposeDone ? ' (propose done)' : ''}`, ...g.nudges.map((n ) => ` ${n}`), ...(g.conflicts || []).map((c ) => ` conflict: ${c.reason}${c.text ? ` (dropped text: ${JSON.stringify(c.text)})` : ''}`));
|
|
63
63
|
const f = r.features;
|
|
64
64
|
// The finished and awaiting-release lists only grow, so the text shows their bounded form.
|
|
65
|
-
const bounded = (key
|
|
65
|
+
const bounded = (key ) => (r.featuresShown && r.featuresShown[key]) || { count: f[key].length, shown: f[key], more: 0 };
|
|
66
66
|
const names = [['finished', bounded('finished')], ['awaiting release', bounded('awaitingRelease')],
|
|
67
67
|
['in progress', { count: f.inProgress.length, shown: f.inProgress, more: 0 }], ['unknown', { count: f.unknown.length, shown: f.unknown, more: 0 }]].filter(([, b]) => b.count);
|
|
68
68
|
for (const [label, b] of names) {
|
|
@@ -72,12 +72,12 @@ function overviewLines(r: any) {
|
|
|
72
72
|
if (r.mode === 'maintain') lines.push(`pending: ${r.pending}`);
|
|
73
73
|
if (r.note) lines.push(`note: ${r.note}`);
|
|
74
74
|
if (r.conflicts.length) lines.push(`${r.conflicts.length} event conflict${r.conflicts.length === 1 ? '' : 's'} (see --json)`);
|
|
75
|
-
if (r.unreadable.length) lines.push(`unreadable event files: ${r.unreadable.map((n
|
|
75
|
+
if (r.unreadable.length) lines.push(`unreadable event files: ${r.unreadable.map((n ) => (r.unreadableReasons && r.unreadableReasons[n] ? `${n} (${r.unreadableReasons[n]})` : n)).join(', ')}`);
|
|
76
76
|
for (const line of r.next) lines.push(`next: ${line}`);
|
|
77
77
|
return lines;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function releaseLines(r
|
|
80
|
+
function releaseLines(r ) {
|
|
81
81
|
const lines = [`${r.recorded ? 'recorded' : 'preview of'} release ${r.tag} (previous ${r.previousTag || 'none'}, bound ${r.bound})`];
|
|
82
82
|
for (const c of r.candidates) if (!r.excluded.includes(c.slug)) lines.push(` ships ${c.slug} (${c.evidence}${c.ref ? ` ${c.ref}` : ''})`);
|
|
83
83
|
if (r.added.length) lines.push(`added: ${r.added.join(', ')}`);
|
|
@@ -89,7 +89,7 @@ function releaseLines(r: any) {
|
|
|
89
89
|
return lines;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function lifecycleLines(result
|
|
92
|
+
function lifecycleLines(result ) {
|
|
93
93
|
switch (result.action) {
|
|
94
94
|
case 'init': return [`${result.slug}: tracked ${result.tracked}${result.taken.length ? `, took ${result.taken.join(', ')}` : ''}${result.goal ? `, serves ${result.goal}` : ''}`, ...result.next];
|
|
95
95
|
case 'status': return [`${result.slug}: ${result.status}${result.evidence ? ` (${result.evidence.kind}${result.evidence.ref ? ` ${result.evidence.ref}` : ''})` : ''}${result.release ? `, release ${result.release}` : ''}`, ...(result.note ? [`note: ${result.note}`] : [])];
|
|
@@ -99,12 +99,12 @@ function lifecycleLines(result: any) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function goalLines(r
|
|
103
|
-
const met = (g
|
|
102
|
+
function goalLines(r ) {
|
|
103
|
+
const met = (g ) => `${g.progress.met}/${g.progress.total} items met`;
|
|
104
104
|
switch (r.action) {
|
|
105
|
-
case 'add': return [`added goal ${r.goal}: ${r.outcome}`, ...r.items.map((i
|
|
106
|
-
case 'item': return [`added ${r.item.id} to ${r.goal}: ${r.item.text}`, ...r.next.map((n
|
|
107
|
-
case 'check': return [`${r.goal} ${r.item}: ${r.met ? 'met' : 'unmet'} (${r.evidence}); ${r.progress.met}/${r.progress.total} items met${r.proposeDone ? ' (propose done)' : ''}`, ...r.next.map((n
|
|
105
|
+
case 'add': return [`added goal ${r.goal}: ${r.outcome}`, ...r.items.map((i ) => ` ${i.id} ${i.text}`), ...r.next.map((n ) => `next: ${n}`)];
|
|
106
|
+
case 'item': return [`added ${r.item.id} to ${r.goal}: ${r.item.text}`, ...r.next.map((n ) => `next: ${n}`)];
|
|
107
|
+
case 'check': return [`${r.goal} ${r.item}: ${r.met ? 'met' : 'unmet'} (${r.evidence}); ${r.progress.met}/${r.progress.total} items met${r.proposeDone ? ' (propose done)' : ''}`, ...r.next.map((n ) => `next: ${n}`)];
|
|
108
108
|
case 'link': return [r.linked === 'already' ? `${r.slug} already serves ${r.goal}` : `${r.slug} now serves ${r.goal}${r.replaced ? ` (was ${r.replaced})` : ''}`];
|
|
109
109
|
case 'done': return [`${r.goal} is done${r.unmet.length ? ` with ${r.unmet.join(', ')} unmet: ${r.reason}` : ''}`];
|
|
110
110
|
default: {
|
|
@@ -119,13 +119,13 @@ function goalLines(r: any) {
|
|
|
119
119
|
for (const c of g.conflicts || []) lines.push(` conflict: ${c.reason}${c.text ? ` (dropped text: ${JSON.stringify(c.text)})` : ''}`);
|
|
120
120
|
}
|
|
121
121
|
if (r.reason) lines.push(`note: ${r.reason}`);
|
|
122
|
-
return [...lines, ...r.next.map((n
|
|
122
|
+
return [...lines, ...r.next.map((n ) => `next: ${n}`)];
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/** Prints a result and sets the exit status; shared by the verbs. */
|
|
128
|
-
function emit(result
|
|
128
|
+
function emit(result , json , lines ) {
|
|
129
129
|
if (json) console.log(JSON.stringify(result, null, 2));
|
|
130
130
|
else if (result.ok === false) console.log(printSafe(`${result.finding}: ${result.message}`));
|
|
131
131
|
else for (const line of lines(result)) console.log(printSafe(line));
|
|
@@ -133,10 +133,10 @@ function emit(result: any, json: any, lines: any) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/** Runs `fn`, turning a caller mistake into the exit-2 usage result and a lock that never came into the `store-locked` refusal. */
|
|
136
|
-
function guarded(verb
|
|
136
|
+
function guarded(verb , action , json , fn ) {
|
|
137
137
|
try {
|
|
138
138
|
return fn();
|
|
139
|
-
} catch (error
|
|
139
|
+
} catch (error ) {
|
|
140
140
|
if (error instanceof followup.FollowupUsageError || error instanceof StoreUnsafeError) return usageError(verb, error.message, json);
|
|
141
141
|
if (error instanceof StoreLockedError) return emit({ ok: false, action, finding: 'store-locked', message: error.message }, json, () => []);
|
|
142
142
|
throw error;
|
|
@@ -144,13 +144,13 @@ function guarded(verb: any, action: any, json: any, fn: any) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/** Runs the store upkeep for one invocation and prints its lines to stderr; returns its refusal, or null. */
|
|
147
|
-
function upkeep(verb
|
|
147
|
+
function upkeep(verb , action , root ) {
|
|
148
148
|
const prepared = prepareStore(root);
|
|
149
149
|
for (const line of prepared.lines) console.error(printSafe(`doflow ${verb}: ${line}`));
|
|
150
150
|
return prepared.ok ? null : { ok: false, action, finding: prepared.finding, message: prepared.message };
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function refuseGlobal(verb
|
|
153
|
+
function refuseGlobal(verb , global , json ) {
|
|
154
154
|
return global ? usageError(verb, `${verb} works on the project store at the repository root and refuses -g`, json) : null;
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -162,7 +162,7 @@ function refuseGlobal(verb: any, global: any, json: any) {
|
|
|
162
162
|
* @param {Object} options.flags parsed flag values: statement, stage, source, taskClass, taskId, release,
|
|
163
163
|
* batch, channel, state, ids, as, reason, evidence, title; for report also file, stdin, text, feature
|
|
164
164
|
*/
|
|
165
|
-
function handleFollowupCommand({ action, cwd, global = false, slug = null, json = false, flags = {} }
|
|
165
|
+
function handleFollowupCommand({ action, cwd, global = false, slug = null, json = false, flags = {} } = {}) {
|
|
166
166
|
const refused = refuseGlobal('followup', global, json);
|
|
167
167
|
if (refused !== null) return refused;
|
|
168
168
|
return guarded('followup', action, json, () => {
|
|
@@ -213,7 +213,7 @@ function handleFollowupCommand({ action, cwd, global = false, slug = null, json
|
|
|
213
213
|
* @param {Object} options.flags parsed flag values: take, intent, goal, maintain, since, tag, confirm,
|
|
214
214
|
* feature, exclude, reason, channel
|
|
215
215
|
*/
|
|
216
|
-
function handleLifecycleCommand({ action, cwd, global = false, slug = null, json = false, flags = {} }
|
|
216
|
+
function handleLifecycleCommand({ action, cwd, global = false, slug = null, json = false, flags = {} } = {}) {
|
|
217
217
|
const refused = refuseGlobal('lifecycle', global, json);
|
|
218
218
|
if (refused !== null) return refused;
|
|
219
219
|
return guarded('lifecycle', action === undefined ? 'overview' : action, json, () => {
|
|
@@ -246,7 +246,7 @@ function handleLifecycleCommand({ action, cwd, global = false, slug = null, json
|
|
|
246
246
|
* @param {Object} options.flags parsed flag values: goal, statement, item (a list), text, evidence, unmet,
|
|
247
247
|
* replace, reason, channel; `--slug` arrives as `slug`
|
|
248
248
|
*/
|
|
249
|
-
function handleGoalCommand({ action, cwd, global = false, slug = null, json = false, flags = {} }
|
|
249
|
+
function handleGoalCommand({ action, cwd, global = false, slug = null, json = false, flags = {} } = {}) {
|
|
250
250
|
const refused = refuseGlobal('goal', global, json);
|
|
251
251
|
if (refused !== null) return refused;
|
|
252
252
|
return guarded('goal', action, json, () => {
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
const nodeFs = require('node:fs');
|
|
18
18
|
const path = require('node:path');
|
|
19
19
|
const crypto = require('node:crypto');
|
|
20
|
-
const { acquireLock } = require('../task-state.
|
|
21
|
-
const { printSafe } = require('../mask.
|
|
22
|
-
const { foldInto, finalize, applyEvent } = require('./fold.
|
|
20
|
+
const { acquireLock } = require('../task-state.js');
|
|
21
|
+
const { printSafe } = require('../mask.js');
|
|
22
|
+
const { foldInto, finalize, applyEvent } = require('./fold.js');
|
|
23
23
|
|
|
24
24
|
const LIFECYCLE_REL = path.join('.doflow', 'state', 'lifecycle');
|
|
25
25
|
const EVENTS_REL = path.join(LIFECYCLE_REL, 'events');
|
|
@@ -39,39 +39,39 @@ const MAX_EVENT_BYTES = 256 * 1024;
|
|
|
39
39
|
const MAX_JOURNAL_BYTES = 16 * 1024 * 1024;
|
|
40
40
|
const READ_CHUNK = 64 * 1024;
|
|
41
41
|
/** The decision register's channel vocabulary: who the caller says is acting. */
|
|
42
|
-
const CHANNEL_BY
|
|
42
|
+
const CHANNEL_BY = { question: 'user', gate: 'user', prompt: 'user', default: 'agent' };
|
|
43
43
|
|
|
44
|
-
function eventsDir(root
|
|
45
|
-
function lockTarget(root
|
|
44
|
+
function eventsDir(root ) { return path.join(root, EVENTS_REL); }
|
|
45
|
+
function lockTarget(root ) { return path.join(root, EVENTS_REL); }
|
|
46
46
|
|
|
47
47
|
/** @param {string} [channel] defaults to `default` @returns {'user'|'agent'|null} null for an unknown channel */
|
|
48
|
-
function byFromChannel(channel
|
|
48
|
+
function byFromChannel(channel = 'default') { return CHANNEL_BY[channel] ?? null; }
|
|
49
49
|
|
|
50
50
|
/** `length` random characters of the lowercase Crockford base32 alphabet. */
|
|
51
|
-
function randomChars(length
|
|
51
|
+
function randomChars(length ) {
|
|
52
52
|
const bytes = crypto.randomBytes(length);
|
|
53
53
|
let out = '';
|
|
54
54
|
for (const b of bytes) out += ALPHABET[b % 32];
|
|
55
55
|
return out;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function idFor(at
|
|
58
|
+
function idFor(at , random ) { return `${at.replace(/[-:.]/g, '')}-${random(6)}`; }
|
|
59
59
|
|
|
60
60
|
/** Synchronous sleep without spinning, as the store lock waits. */
|
|
61
|
-
function sleep(ms
|
|
61
|
+
function sleep(ms ) {
|
|
62
62
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** DEC-045: `at` is a strict UTC timestamp that is a real instant, so year 10000 and "+275760-..." never parse. */
|
|
66
66
|
const STRICT_AT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
67
|
-
function isStrictAt(at
|
|
67
|
+
function isStrictAt(at ) {
|
|
68
68
|
if (typeof at !== 'string' || !STRICT_AT.test(at)) return false;
|
|
69
69
|
const ms = Date.parse(at);
|
|
70
70
|
return Number.isFinite(ms) && new Date(ms).toISOString() === at;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** An IC-002 envelope whose `at` is strict and equals the time prefix of its file id. */
|
|
74
|
-
function isEnvelope(event
|
|
74
|
+
function isEnvelope(event , idFromName ) {
|
|
75
75
|
return event && typeof event === 'object' && event.v === 1 && event.id === idFromName
|
|
76
76
|
&& typeof event.type === 'string' && typeof event.by === 'string'
|
|
77
77
|
&& isStrictAt(event.at) && idFromName.startsWith(`${event.at.replace(/[-:.]/g, '')}-`)
|
|
@@ -84,7 +84,7 @@ class StoreUnsafeError extends Error {}
|
|
|
84
84
|
/** The store lock could not be taken in time; the verb refuses with `store-locked` and writes nothing. */
|
|
85
85
|
class StoreLockedError extends Error {}
|
|
86
86
|
|
|
87
|
-
function assertStoreFolders(root
|
|
87
|
+
function assertStoreFolders(root , fsImpl ) {
|
|
88
88
|
for (const rel of [path.join('.doflow'), path.join('.doflow', 'state'), LIFECYCLE_REL, EVENTS_REL]) {
|
|
89
89
|
const folder = path.join(root, rel);
|
|
90
90
|
let link = false;
|
|
@@ -99,9 +99,9 @@ function assertStoreFolders(root: string, fsImpl: any): void {
|
|
|
99
99
|
* and again by `O_NOFOLLOW` where the platform has it; `O_NONBLOCK` keeps a FIFO swapped in after
|
|
100
100
|
* the `lstat` from blocking the open, and the descriptor is checked again before any read.
|
|
101
101
|
*/
|
|
102
|
-
function readEventFile(fsImpl
|
|
102
|
+
function readEventFile(fsImpl , file , maxBytes = MAX_EVENT_BYTES) {
|
|
103
103
|
const flags = nodeFs.constants;
|
|
104
|
-
let fd
|
|
104
|
+
let fd ;
|
|
105
105
|
try {
|
|
106
106
|
if (!fsImpl.lstatSync(file).isFile()) return { reason: 'not a regular file' };
|
|
107
107
|
fd = fsImpl.openSync(file, flags.O_RDONLY | (flags.O_NOFOLLOW || 0) | (flags.O_NONBLOCK || 0));
|
|
@@ -110,7 +110,7 @@ function readEventFile(fsImpl: any, file: string, maxBytes: number = MAX_EVENT_B
|
|
|
110
110
|
const tooLarge = { reason: `larger than ${maxBytes / 1024} KiB` };
|
|
111
111
|
if (st.size > maxBytes) return tooLarge;
|
|
112
112
|
// Read in chunks up to one byte past the cap, so a file that grew after the fstat is still caught.
|
|
113
|
-
const chunks
|
|
113
|
+
const chunks = [];
|
|
114
114
|
let length = 0;
|
|
115
115
|
while (length <= maxBytes) {
|
|
116
116
|
const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK, maxBytes + 1 - length));
|
|
@@ -120,7 +120,7 @@ function readEventFile(fsImpl: any, file: string, maxBytes: number = MAX_EVENT_B
|
|
|
120
120
|
length += n;
|
|
121
121
|
}
|
|
122
122
|
return length > maxBytes ? tooLarge : { text: Buffer.concat(chunks, length).toString('utf8') };
|
|
123
|
-
} catch (error
|
|
123
|
+
} catch (error ) {
|
|
124
124
|
return { reason: error.code || 'unreadable' };
|
|
125
125
|
} finally {
|
|
126
126
|
if (fd !== undefined) { try { fsImpl.closeSync(fd); } catch { /* read already done */ } }
|
|
@@ -128,16 +128,16 @@ function readEventFile(fsImpl: any, file: string, maxBytes: number = MAX_EVENT_B
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/** The event with every string print-safe; a report excerpt is multi-line by design, so it keeps its line breaks (cli.js removes them when printing). */
|
|
131
|
-
function cleanEvent(event
|
|
131
|
+
function cleanEvent(event ) {
|
|
132
132
|
const clean = printSafe(event);
|
|
133
133
|
if (typeof event.data.excerpt === 'string') clean.data.excerpt = printSafe(event.data.excerpt, { keepLineBreaks: true });
|
|
134
134
|
return clean;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** Whether `name` is `<EVENT_ID>.json`. */
|
|
138
|
-
function isEventName(name
|
|
138
|
+
function isEventName(name ) {
|
|
139
139
|
const match = /^(.+)\.json$/.exec(name);
|
|
140
|
-
return Boolean(match) && EVENT_ID.test((match
|
|
140
|
+
return Boolean(match) && EVENT_ID.test((match )[1]);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
@@ -146,39 +146,39 @@ function isEventName(name: string): boolean {
|
|
|
146
146
|
* pending; a pending name that is not an event file name is ignored.
|
|
147
147
|
* @returns {{generation: string|null, pending: string[]}}
|
|
148
148
|
*/
|
|
149
|
-
function readJournal(root
|
|
149
|
+
function readJournal(root , fsImpl = nodeFs) {
|
|
150
150
|
const read = readEventFile(fsImpl, path.join(root, JOURNAL_REL), MAX_JOURNAL_BYTES);
|
|
151
|
-
let journal
|
|
152
|
-
try { journal = read.reason ? null : JSON.parse(read.text
|
|
151
|
+
let journal ;
|
|
152
|
+
try { journal = read.reason ? null : JSON.parse(read.text ); } catch { journal = null; }
|
|
153
153
|
if (!journal || typeof journal !== 'object' || journal.v !== 1) return { generation: null, pending: [] };
|
|
154
154
|
return {
|
|
155
155
|
generation: typeof journal.generation === 'string' ? journal.generation : null,
|
|
156
|
-
pending: Array.isArray(journal.pending) ? journal.pending.filter((name
|
|
156
|
+
pending: Array.isArray(journal.pending) ? journal.pending.filter((name ) => typeof name === 'string' && isEventName(name)) : [],
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/** One pass over the event files, leaving out the names in `hidden`. */
|
|
161
|
-
function readEventFiles(root
|
|
161
|
+
function readEventFiles(root , fsImpl , hidden , shouldStop ) {
|
|
162
162
|
const dir = eventsDir(root);
|
|
163
|
-
let names
|
|
163
|
+
let names ;
|
|
164
164
|
try {
|
|
165
165
|
names = fsImpl.readdirSync(dir);
|
|
166
|
-
} catch (error
|
|
166
|
+
} catch (error ) {
|
|
167
167
|
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return { events: [], unreadable: [], reasons: {} };
|
|
168
168
|
throw error;
|
|
169
169
|
}
|
|
170
170
|
const orderedNames = names.sort();
|
|
171
171
|
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
172
|
-
const events
|
|
173
|
-
const unreadable
|
|
174
|
-
const reasons
|
|
172
|
+
const events = [];
|
|
173
|
+
const unreadable = [];
|
|
174
|
+
const reasons = {};
|
|
175
175
|
for (const name of orderedNames) {
|
|
176
176
|
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
177
177
|
if (!isEventName(name) || hidden.has(name)) continue;
|
|
178
178
|
const read = readEventFile(fsImpl, path.join(dir, name));
|
|
179
179
|
if (read.reason) { unreadable.push(name); reasons[name] = read.reason; continue; }
|
|
180
180
|
try {
|
|
181
|
-
const event = JSON.parse(read.text
|
|
181
|
+
const event = JSON.parse(read.text );
|
|
182
182
|
// Someone else's file is shown as it is read, so its text is made print-safe here (stored bytes are not changed).
|
|
183
183
|
if (isEnvelope(event, name.slice(0, -'.json'.length))) events.push(cleanEvent(event)); else unreadable.push(name);
|
|
184
184
|
} catch {
|
|
@@ -204,7 +204,7 @@ function readEventFiles(root: string, fsImpl: any, hidden: Set<string>, shouldSt
|
|
|
204
204
|
* StoreLockedError.
|
|
205
205
|
* @returns {{events: Object[], unreadable: string[], reasons: Object<string,string>, incomplete?: true}}
|
|
206
206
|
*/
|
|
207
|
-
function readEvents(root
|
|
207
|
+
function readEvents(root , { fsImpl = nodeFs, lockFallback = true, shouldStop } = {}) {
|
|
208
208
|
assertStoreFolders(root, fsImpl);
|
|
209
209
|
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
210
210
|
for (let attempt = 1; attempt <= STABLE_READS; attempt += 1) {
|
|
@@ -220,10 +220,10 @@ function readEvents(root: string, { fsImpl = nodeFs, lockFallback = true, should
|
|
|
220
220
|
}
|
|
221
221
|
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
222
222
|
if (!lockFallback) throw new StoreLockedError('Lifecycle event store changed during a read; a stable snapshot was unavailable.');
|
|
223
|
-
let release
|
|
223
|
+
let release ;
|
|
224
224
|
try {
|
|
225
225
|
release = acquireLock(fsImpl, lockTarget(root));
|
|
226
|
-
} catch (error
|
|
226
|
+
} catch (error ) {
|
|
227
227
|
if (/^Could not lock/.test(error.message || '')) throw new StoreLockedError(`${error.message} Nothing was written.`);
|
|
228
228
|
throw error;
|
|
229
229
|
}
|
|
@@ -240,20 +240,20 @@ function readEvents(root: string, { fsImpl = nodeFs, lockFallback = true, should
|
|
|
240
240
|
* @param {{hasBody?: Function, fsImpl?: Object}} [options]
|
|
241
241
|
* @returns {Object} the fold result (see fold.js) plus `unreadable`
|
|
242
242
|
*/
|
|
243
|
-
function readFold(root
|
|
243
|
+
function readFold(root , { fsImpl = nodeFs, hasBody, now = new Date() } = {}) {
|
|
244
244
|
const { events, unreadable, reasons } = readEvents(root, { fsImpl });
|
|
245
245
|
return { ...finalize(foldInto(events, { now }), { hasBody }), unreadable, unreadableReasons: reasons };
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/** Picks a free, valid id for each draft before any file is created, so a batch is all or nothing. */
|
|
249
|
-
function allocateIds(fsImpl
|
|
250
|
-
const chosen = new Set
|
|
251
|
-
const ids
|
|
249
|
+
function allocateIds(fsImpl , dir , stamped , random ) {
|
|
250
|
+
const chosen = new Set ();
|
|
251
|
+
const ids = [];
|
|
252
252
|
for (const { at, id } of stamped) {
|
|
253
|
-
let candidate
|
|
253
|
+
let candidate = id;
|
|
254
254
|
for (let attempt = 0; attempt <= COLLISION_RETRIES; attempt += 1) {
|
|
255
255
|
if (attempt > 0) candidate = idFor(at, random);
|
|
256
|
-
if (EVENT_ID.test(candidate
|
|
256
|
+
if (EVENT_ID.test(candidate ) && !chosen.has(candidate ) && !fsImpl.existsSync(path.join(dir, `${candidate}.json`))) break;
|
|
257
257
|
candidate = null;
|
|
258
258
|
}
|
|
259
259
|
if (!candidate) return null;
|
|
@@ -276,17 +276,17 @@ function allocateIds(fsImpl: any, dir: string, stamped: any[], random: (n: numbe
|
|
|
276
276
|
* conflict, with its own `code` in `conflict.code`), `id-collision`, `invalid-id` (the clock or
|
|
277
277
|
* the random source cannot produce an id of the IC-001 shape) and `store-locked`.
|
|
278
278
|
*/
|
|
279
|
-
function appendEvents(root
|
|
279
|
+
function appendEvents(root , drafts , { now = new Date(), fsImpl = nodeFs, random = randomChars } = {}) {
|
|
280
280
|
const dir = eventsDir(root);
|
|
281
281
|
// A write that would be refused leaves nothing behind, not even the store folder (IC-001: created
|
|
282
282
|
// on the first write), so the check runs once before the folder and the lock and again under the lock.
|
|
283
283
|
const early = planEvents(root, drafts, { now, fsImpl, random });
|
|
284
284
|
if (early.refused) return early.refused;
|
|
285
285
|
fsImpl.mkdirSync(dir, { recursive: true });
|
|
286
|
-
let release
|
|
286
|
+
let release ;
|
|
287
287
|
try {
|
|
288
288
|
release = acquireLock(fsImpl, lockTarget(root));
|
|
289
|
-
} catch (error
|
|
289
|
+
} catch (error ) {
|
|
290
290
|
if (/^Could not lock/.test(error.message || '')) return { ok: false, finding: 'store-locked', message: `${error.message} Nothing was written.` };
|
|
291
291
|
throw error;
|
|
292
292
|
}
|
|
@@ -295,8 +295,8 @@ function appendEvents(root: string, drafts: any[], { now = new Date(), fsImpl =
|
|
|
295
295
|
if (refused) return refused;
|
|
296
296
|
const ids = allocateIds(fsImpl, dir, stamped, random);
|
|
297
297
|
if (!ids) return { ok: false, finding: 'id-collision', written: [], message: `could not find a free event id after ${COLLISION_RETRIES} tries. Nothing was written.` };
|
|
298
|
-
const written
|
|
299
|
-
stamped.forEach(({ draft, at }
|
|
298
|
+
const written = [];
|
|
299
|
+
stamped.forEach(({ draft, at } , index ) => {
|
|
300
300
|
const event = { v: 1, id: ids[index], type: draft.type, at, by: draft.by, data: draft.data };
|
|
301
301
|
const file = path.join(dir, `${event.id}.json`);
|
|
302
302
|
// The ids are free under the lock; the exclusive create only guards against a writer outside it.
|
|
@@ -310,15 +310,15 @@ function appendEvents(root: string, drafts: any[], { now = new Date(), fsImpl =
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/** Folds the store, stamps each draft and applies it; `refused` is the whole call's refusal when any is illegal. */
|
|
313
|
-
function planEvents(root
|
|
313
|
+
function planEvents(root , drafts , { now, fsImpl, random } ) {
|
|
314
314
|
const state = foldInto(readEvents(root, { fsImpl }).events, { now });
|
|
315
315
|
// An event dated far ahead is not in the fold (DEC-045), so it cannot push this stamp forward.
|
|
316
316
|
let floor = state.newestAt === null ? -Infinity : state.newestAt + 1;
|
|
317
|
-
const stamped
|
|
317
|
+
const stamped = [];
|
|
318
318
|
for (const draft of drafts) {
|
|
319
319
|
const atMs = Math.max(now.getTime(), floor);
|
|
320
320
|
floor = atMs + 1;
|
|
321
|
-
let at
|
|
321
|
+
let at ;
|
|
322
322
|
try { at = new Date(atMs).toISOString(); } catch { at = null; }
|
|
323
323
|
// The dry run draws the id the write will try first, so a conflict names the event it refuses.
|
|
324
324
|
const id = at && isStrictAt(at) ? idFor(at, random) : null;
|