@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
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const { workStatus, createWorkCache, refreshedWorkCache } = require('./work-status.
|
|
8
|
-
const { projectKey } = require('../lifecycle/report-store.
|
|
9
|
-
const { readEvents } = require('../lifecycle/event-store.
|
|
7
|
+
const { workStatus, createWorkCache, refreshedWorkCache } = require('./work-status.js');
|
|
8
|
+
const { projectKey } = require('../lifecycle/report-store.js');
|
|
9
|
+
const { readEvents } = require('../lifecycle/event-store.js');
|
|
10
10
|
|
|
11
11
|
const DAY = 86400000;
|
|
12
12
|
const HOUR = 3600000;
|
|
@@ -18,12 +18,12 @@ const removableRules = new Set(['count-and-age','work','date-partition','recover
|
|
|
18
18
|
const SAFE_NAMESPACE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
19
19
|
// Open-log protection cannot vouch for a record store that holds a link, so the pass fails closed.
|
|
20
20
|
const LINKED = 'link-in-record-store';
|
|
21
|
-
function stopped(context
|
|
21
|
+
function stopped(context ) { return Boolean(context.shouldStop?.()); }
|
|
22
22
|
function budgetError() { return Object.assign(new Error('storage pass budget exhausted'), { code: 'EBUDGET', partial: true }); }
|
|
23
|
-
function projectRoot(roots
|
|
24
|
-
function safeStat(fsImpl
|
|
25
|
-
function names(fsImpl
|
|
26
|
-
function ageReason(store
|
|
23
|
+
function projectRoot(roots ) { return roots['project-state'] ? path.dirname(path.dirname(roots['project-state'])) : null; }
|
|
24
|
+
function safeStat(fsImpl , file ) { try { return fsImpl.lstatSync(file); } catch { return null; } }
|
|
25
|
+
function names(fsImpl , dir ) { return fsImpl.readdirSync(dir); }
|
|
26
|
+
function ageReason(store , entry , now , fsImpl , storeDir , shouldStop ) {
|
|
27
27
|
const limit = store.bound.maxAgeDays * DAY;
|
|
28
28
|
if (store.rule === 'date-partition') {
|
|
29
29
|
const date = entry.name.slice(0, 10);
|
|
@@ -45,13 +45,13 @@ function ageReason(store: any, entry: any, now: any, fsImpl: any, storeDir: any,
|
|
|
45
45
|
}
|
|
46
46
|
return now - newest > limit ? 'age' : null;
|
|
47
47
|
}
|
|
48
|
-
function workCandidates(store
|
|
48
|
+
function workCandidates(store , entries , context ) {
|
|
49
49
|
const { fsImpl, storeDir, now, roots, settings, shouldStop } = context;
|
|
50
50
|
const project = projectRoot(roots);
|
|
51
51
|
if (!project) return [];
|
|
52
52
|
const cache = context.cache || createWorkCache({ projectRoot: project, fsImpl });
|
|
53
|
-
const recordMatch = new RegExp(store.match), selected
|
|
54
|
-
const visit = (dir
|
|
53
|
+
const recordMatch = new RegExp(store.match), selected = [];
|
|
54
|
+
const visit = (dir , namespace = null) => {
|
|
55
55
|
let children;
|
|
56
56
|
try { children = names(fsImpl, dir); } catch { return true; }
|
|
57
57
|
for (const name of children) {
|
|
@@ -77,7 +77,7 @@ function workCandidates(store: any, entries: any, context: any) {
|
|
|
77
77
|
return selected;
|
|
78
78
|
}
|
|
79
79
|
/** Revalidate the subject under its try-lock against its current run and plan, not the selection snapshot. */
|
|
80
|
-
function recordStillCandidate(store
|
|
80
|
+
function recordStillCandidate(store , candidate , info , context ) {
|
|
81
81
|
const { fsImpl, roots, now, settings, cache, shouldStop, storeDir } = context;
|
|
82
82
|
const before = candidate.recordStat;
|
|
83
83
|
if (!info.isFile() || !before || ['mtimeMs', 'ctimeMs', 'size', 'ino', 'dev'].some(key => info[key] !== before[key])) return false;
|
|
@@ -96,7 +96,7 @@ function recordStillCandidate(store: any, candidate: any, info: any, context: an
|
|
|
96
96
|
return !status.incomplete && !shouldStop?.() && status.state === 'finished'
|
|
97
97
|
&& status.since === candidate.mtimeMs && now - status.since > store.bound.finishedDays * DAY;
|
|
98
98
|
}
|
|
99
|
-
function workStorePathStatus(store
|
|
99
|
+
function workStorePathStatus(store , roots , fsImpl ) {
|
|
100
100
|
const project = projectRoot(roots), root = roots[store.root];
|
|
101
101
|
if (!project || !root) return 'missing';
|
|
102
102
|
const paths = [path.join(project, '.doflow'), root];
|
|
@@ -105,16 +105,16 @@ function workStorePathStatus(store: any, roots: any, fsImpl: any) {
|
|
|
105
105
|
for (const candidate of paths) {
|
|
106
106
|
let info;
|
|
107
107
|
try { info = fsImpl.lstatSync(candidate); }
|
|
108
|
-
catch (error
|
|
108
|
+
catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR' ? 'missing' : 'inaccessible'; }
|
|
109
109
|
if (info.isSymbolicLink()) return 'linked';
|
|
110
110
|
}
|
|
111
111
|
return 'safe';
|
|
112
112
|
}
|
|
113
|
-
function protectedLogs({ registry, roots, fsImpl, cache, shouldStop }
|
|
113
|
+
function protectedLogs({ registry, roots, fsImpl, cache, shouldStop } ) {
|
|
114
114
|
const project = projectRoot(roots), protectedNames = new Set();
|
|
115
115
|
if (!project) return { names: protectedNames, incomplete: false };
|
|
116
116
|
const workCache = cache || createWorkCache({ projectRoot: project, fsImpl });
|
|
117
|
-
for (const store of registry.stores.filter((s
|
|
117
|
+
for (const store of registry.stores.filter((s ) => s.root === 'project-state' && s.entry === 'record' && s.rule === 'work')) {
|
|
118
118
|
if (shouldStop?.()) return { names: protectedNames, incomplete: true };
|
|
119
119
|
const pathStatus = workStorePathStatus(store, roots, fsImpl);
|
|
120
120
|
if (pathStatus === 'missing') continue;
|
|
@@ -122,16 +122,16 @@ function protectedLogs({ registry, roots, fsImpl, cache, shouldStop }: any) {
|
|
|
122
122
|
const dir = path.join(roots[store.root], ...store.path.split('/'));
|
|
123
123
|
const recordMatch = new RegExp(store.match);
|
|
124
124
|
let linked = false;
|
|
125
|
-
const visit = (folder
|
|
125
|
+
const visit = (folder , namespace = null) => {
|
|
126
126
|
let list;
|
|
127
127
|
try { list = names(fsImpl, folder); }
|
|
128
|
-
catch (error
|
|
128
|
+
catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR'; }
|
|
129
129
|
for (const name of list) {
|
|
130
130
|
if (shouldStop?.()) return false;
|
|
131
131
|
const file = path.join(folder, name);
|
|
132
132
|
let stat;
|
|
133
133
|
try { stat = fsImpl.lstatSync(file); }
|
|
134
|
-
catch (error
|
|
134
|
+
catch (error ) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') continue; return false; }
|
|
135
135
|
if (stat.isSymbolicLink()) { linked = true; return false; }
|
|
136
136
|
if (namespace === null && stat.isDirectory() && SAFE_NAMESPACE.test(name) && !name.includes('..')) {
|
|
137
137
|
if (!visit(file, name)) return false;
|
|
@@ -158,18 +158,18 @@ function protectedLogs({ registry, roots, fsImpl, cache, shouldStop }: any) {
|
|
|
158
158
|
}
|
|
159
159
|
return { names: protectedNames, incomplete: false };
|
|
160
160
|
}
|
|
161
|
-
function lifecyclePathsSafe(project
|
|
161
|
+
function lifecyclePathsSafe(project , fsImpl ) {
|
|
162
162
|
for (const rel of [path.join('.doflow'), path.join('.doflow', 'state'), path.join('.doflow', 'state', 'lifecycle'), path.join('.doflow', 'state', 'lifecycle', 'events')]) {
|
|
163
163
|
try { if (fsImpl.lstatSync(path.join(project, rel)).isSymbolicLink()) return false; }
|
|
164
|
-
catch (error
|
|
164
|
+
catch (error ) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') return false; if (rel !== path.join('.doflow', 'state', 'lifecycle', 'events')) return false; }
|
|
165
165
|
}
|
|
166
166
|
return true;
|
|
167
167
|
}
|
|
168
|
-
function definitelyMissing(fsImpl
|
|
168
|
+
function definitelyMissing(fsImpl , target ) {
|
|
169
169
|
try { fsImpl.lstatSync(target); return false; }
|
|
170
|
-
catch (error
|
|
170
|
+
catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR'; }
|
|
171
171
|
}
|
|
172
|
-
function reportCandidates(store
|
|
172
|
+
function reportCandidates(store , entries , context ) {
|
|
173
173
|
const { fsImpl, storeDir, roots, now, settings, shouldStop } = context;
|
|
174
174
|
const project = projectRoot(roots);
|
|
175
175
|
const key = project && projectKey(project, fsImpl), selected = [];
|
|
@@ -183,7 +183,7 @@ function reportCandidates(store: any, entries: any, context: any) {
|
|
|
183
183
|
if (shouldStop?.()) throw budgetError();
|
|
184
184
|
if (BODY.test(name) && safeStat(fsImpl, path.join(folder, name))?.isFile()) bodyNames.push(name);
|
|
185
185
|
}
|
|
186
|
-
} catch (error
|
|
186
|
+
} catch (error ) { if (error.partial) throw error; continue; }
|
|
187
187
|
if (entry.name === key) {
|
|
188
188
|
const lifecycle = path.join(project, '.doflow/state/lifecycle');
|
|
189
189
|
if (!safeStat(fsImpl, path.join(lifecycle, 'migrated.json'))?.isFile() || !lifecyclePathsSafe(project, fsImpl)) continue;
|
|
@@ -232,7 +232,7 @@ function reportCandidates(store: any, entries: any, context: any) {
|
|
|
232
232
|
}
|
|
233
233
|
return selected;
|
|
234
234
|
}
|
|
235
|
-
function selectCandidates(store
|
|
235
|
+
function selectCandidates(store , entries , context ) {
|
|
236
236
|
const { now, fsImpl, storeDir, roots, shouldStop } = context;
|
|
237
237
|
switch (store.rule) {
|
|
238
238
|
case 'count-and-age': {
|
|
@@ -5,39 +5,39 @@
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { randomBytes } = require('node:crypto');
|
|
8
|
-
const { REPO_ROOT } = require('../../helper/repo-root.
|
|
9
|
-
const { loadStorageRegistry } = require('./registry.
|
|
10
|
-
const { resolveRoots, readPruneSetting } = require('./roots.
|
|
11
|
-
const engine = require('./engine.
|
|
12
|
-
const { RunLedger } = require('../trace/ledger.
|
|
8
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
9
|
+
const { loadStorageRegistry } = require('./registry.js');
|
|
10
|
+
const { resolveRoots, readPruneSetting } = require('./roots.js');
|
|
11
|
+
const engine = require('./engine.js');
|
|
12
|
+
const { RunLedger } = require('../trace/ledger.js');
|
|
13
13
|
|
|
14
14
|
// Load once: a not-due moment only reads the three small markers, never the catalogue again.
|
|
15
|
-
let registry
|
|
15
|
+
let registry ;
|
|
16
16
|
try { registry = loadStorageRegistry({ repoRoot: REPO_ROOT }); } catch { /* fail closed */ }
|
|
17
17
|
const MOMENT_ROOTS = ['project-state', 'user-state', 'config-home'];
|
|
18
18
|
|
|
19
|
-
function silently(fn
|
|
19
|
+
function silently(fn ) {
|
|
20
20
|
const exitCode = process.exitCode;
|
|
21
21
|
try { if (registry) return fn(); } catch { /* upkeep must never affect its caller */ }
|
|
22
22
|
finally { process.exitCode = exitCode; }
|
|
23
23
|
}
|
|
24
|
-
function stat(file
|
|
24
|
+
function stat(file , fsImpl ) {
|
|
25
25
|
try { return fsImpl.lstatSync(file); }
|
|
26
|
-
catch (error
|
|
26
|
+
catch (error ) { if (error.code === 'ENOENT') return null; throw error; }
|
|
27
27
|
}
|
|
28
|
-
function usableRoot(root
|
|
28
|
+
function usableRoot(root , fsImpl ) {
|
|
29
29
|
if (!root) return false;
|
|
30
30
|
const owned = path.basename(root) === 'state' ? path.dirname(root) : root;
|
|
31
31
|
const parent = stat(owned, fsImpl), info = stat(root, fsImpl);
|
|
32
32
|
return !!parent?.isDirectory() && !parent.isSymbolicLink() && !!info?.isDirectory() && !info.isSymbolicLink();
|
|
33
33
|
}
|
|
34
|
-
function atomicWrite(file
|
|
34
|
+
function atomicWrite(file , text , fsImpl , { preserveExisting = false } = {}) {
|
|
35
35
|
const temp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
36
36
|
try {
|
|
37
37
|
fsImpl.writeFileSync(temp, text, { flag: 'wx' });
|
|
38
38
|
if (preserveExisting) {
|
|
39
39
|
// IC-016: a link never replaces a name, so a file created since the caller's check stays.
|
|
40
|
-
try { fsImpl.linkSync(temp, file); } catch (error
|
|
40
|
+
try { fsImpl.linkSync(temp, file); } catch (error ) { if (error.code === 'EEXIST') return false; throw error; }
|
|
41
41
|
return true;
|
|
42
42
|
}
|
|
43
43
|
fsImpl.renameSync(temp, file);
|
|
@@ -47,7 +47,7 @@ function atomicWrite(file: any, text: any, fsImpl: any, { preserveExisting = fal
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
function ignoreBytes() { return `${registry.settings.ignoreFile.lines.join('\n')}\n`; }
|
|
50
|
-
function ensureIgnore({ roots, rootId, projectRoot, fsImpl }
|
|
50
|
+
function ensureIgnore({ roots, rootId, projectRoot, fsImpl } ) {
|
|
51
51
|
const root = roots[rootId];
|
|
52
52
|
if (!usableRoot(root, fsImpl)) return;
|
|
53
53
|
const file = path.join(root, registry.settings.ignoreFile.name);
|
|
@@ -64,7 +64,7 @@ function ensureIgnore({ roots, rootId, projectRoot, fsImpl }: any) {
|
|
|
64
64
|
* never appended. Its own pass (at most daily) adds it once user state exists, so a global
|
|
65
65
|
* uninstall can still find its controls (IC-019).
|
|
66
66
|
*/
|
|
67
|
-
function registerProject(roots
|
|
67
|
+
function registerProject(roots , fsImpl ) {
|
|
68
68
|
const projectState = roots['project-state'], userState = roots['user-state'];
|
|
69
69
|
if (!usableRoot(projectState, fsImpl) || !usableRoot(userState, fsImpl)) return;
|
|
70
70
|
if (!stat(path.join(projectState, registry.settings.ignoreFile.name), fsImpl)?.isFile()) return;
|
|
@@ -75,31 +75,31 @@ function registerProject(roots: any, fsImpl: any) {
|
|
|
75
75
|
if ((indexedRoots(index, fsImpl) || []).includes(project)) return;
|
|
76
76
|
fsImpl.appendFileSync(index, `${project}\n`, 'utf8');
|
|
77
77
|
}
|
|
78
|
-
function indexedRoots(file
|
|
78
|
+
function indexedRoots(file , fsImpl ) {
|
|
79
79
|
const info = stat(file, fsImpl);
|
|
80
80
|
if (!info || info.isSymbolicLink() || !info.isFile()) return null;
|
|
81
|
-
return fsImpl.readFileSync(file, 'utf8').split('\n').filter((root
|
|
81
|
+
return fsImpl.readFileSync(file, 'utf8').split('\n').filter((root ) => path.isAbsolute(root));
|
|
82
82
|
}
|
|
83
|
-
function rewriteRoots(roots
|
|
83
|
+
function rewriteRoots(roots , fsImpl , forget = null) {
|
|
84
84
|
const file = path.join(roots['user-state'], 'upkeep-roots');
|
|
85
85
|
if (!usableRoot(roots['user-state'], fsImpl)) return;
|
|
86
86
|
const lines = indexedRoots(file, fsImpl);
|
|
87
87
|
if (!lines) return;
|
|
88
88
|
const kept = forget === null
|
|
89
89
|
? [...new Set(lines)].filter(root => stat(path.join(root, '.doflow/state/.gitignore'), fsImpl))
|
|
90
|
-
: lines.filter((root
|
|
90
|
+
: lines.filter((root ) => root !== forget);
|
|
91
91
|
atomicWrite(file, kept.length ? `${kept.join('\n')}\n` : '', fsImpl);
|
|
92
92
|
}
|
|
93
|
-
function due(root
|
|
93
|
+
function due(root , now , fsImpl ) {
|
|
94
94
|
try {
|
|
95
95
|
const text = fsImpl.readFileSync(path.join(root, 'upkeep-due'), 'utf8').trim();
|
|
96
96
|
return !/^\d+$/.test(text) || !Number.isFinite(Number(text)) || Number(text) * 1000 <= now;
|
|
97
97
|
} catch { return true; }
|
|
98
98
|
}
|
|
99
|
-
function takeLock(root
|
|
99
|
+
function takeLock(root , now , fsImpl ) {
|
|
100
100
|
const lock = path.join(root, 'upkeep.lock');
|
|
101
101
|
try { fsImpl.mkdirSync(lock); return true; }
|
|
102
|
-
catch (error
|
|
102
|
+
catch (error ) {
|
|
103
103
|
if (error.code !== 'EEXIST') throw error;
|
|
104
104
|
const info = stat(lock, fsImpl);
|
|
105
105
|
if (info?.isDirectory() && !info.isSymbolicLink() && now - info.mtimeMs > registry.settings.passLockStaleSeconds * 1000) {
|
|
@@ -108,12 +108,12 @@ function takeLock(root: any, now: any, fsImpl: any) {
|
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
function writeDue(root
|
|
111
|
+
function writeDue(root , time , fsImpl ) {
|
|
112
112
|
const file = path.join(root, 'upkeep-due');
|
|
113
113
|
if (stat(file, fsImpl)?.isSymbolicLink()) throw new Error('linked upkeep marker');
|
|
114
114
|
atomicWrite(file, `${Math.floor(time / 1000)}\n`, fsImpl);
|
|
115
115
|
}
|
|
116
|
-
function passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled = false }
|
|
116
|
+
function passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled = false } ) {
|
|
117
117
|
const root = roots[rootId];
|
|
118
118
|
if (!usableRoot(root, fsImpl)) return;
|
|
119
119
|
const setting = readPruneSetting({ registry, roots, rootId, fsImpl });
|
|
@@ -124,7 +124,7 @@ function passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled = fals
|
|
|
124
124
|
if (throttled) writeDue(root, now + registry.settings.upkeepIntervalHours * 3600000, fsImpl);
|
|
125
125
|
ran = true;
|
|
126
126
|
result = engine.runPass({ registry, roots, stores, mode: 'prune', setting: { [rootId]: 'on' }, now, env, fsImpl });
|
|
127
|
-
outcome = result.partial ? 'partial' : result.stores.some((store
|
|
127
|
+
outcome = result.partial ? 'partial' : result.stores.some((store ) => store.failed.length) ? 'error' : 'ok';
|
|
128
128
|
if (rootId === 'user-state') rewriteRoots(roots, fsImpl);
|
|
129
129
|
if (rootId === 'project-state') { try { registerProject(roots, fsImpl); } catch { /* the index is best effort */ } }
|
|
130
130
|
} catch { outcome = 'error'; /* the record never includes failure content */ }
|
|
@@ -146,7 +146,7 @@ function passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled = fals
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** IC-009: silent throttled upkeep after a verb, using that verb's resolved store root. */
|
|
149
|
-
function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs }
|
|
149
|
+
function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
|
|
150
150
|
return silently(() => {
|
|
151
151
|
const roots = resolveRoots({ registry, projectRoot, env });
|
|
152
152
|
for (const rootId of ['project-state', 'user-state']) {
|
|
@@ -156,7 +156,7 @@ function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsI
|
|
|
156
156
|
try {
|
|
157
157
|
const root = roots[rootId];
|
|
158
158
|
if (!usableRoot(root, fsImpl) || !due(root, now, fsImpl)) continue;
|
|
159
|
-
const stores = registry.stores.filter((store
|
|
159
|
+
const stores = registry.stores.filter((store ) => store.root === rootId && store.moments.includes('verb')).map((store ) => store.id);
|
|
160
160
|
passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled: true });
|
|
161
161
|
} catch { /* one inaccessible root cannot stop the others */ }
|
|
162
162
|
}
|
|
@@ -164,7 +164,7 @@ function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsI
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/** IC-010: the verify moment is project-only and unthrottled. Wiring belongs to D.3. */
|
|
167
|
-
function upkeepAfterVerify({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs }
|
|
167
|
+
function upkeepAfterVerify({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
|
|
168
168
|
return silently(() => {
|
|
169
169
|
const roots = resolveRoots({ registry, projectRoot, env });
|
|
170
170
|
return passForRoot({ roots, rootId: 'project-state', stores: ['verification-logs'], now, env, fsImpl });
|
|
@@ -172,7 +172,7 @@ function upkeepAfterVerify({ projectRoot, now = Date.now(), env = process.env, f
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/** IC-011: only this scope's recovery store; install/update call sites belong to D.4. */
|
|
175
|
-
function storageAfterInstall({ scope, scopeRoot, now = Date.now(), env = process.env, fsImpl = fs }
|
|
175
|
+
function storageAfterInstall({ scope, scopeRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
|
|
176
176
|
return silently(() => {
|
|
177
177
|
if (!['project', 'global'].includes(scope)) return;
|
|
178
178
|
const global = scope === 'global';
|
|
@@ -184,20 +184,20 @@ function storageAfterInstall({ scope, scopeRoot, now = Date.now(), env = process
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/** IC-019: user-initiated cleanup removes owned controls, never state or user settings. */
|
|
187
|
-
function storageForgetScope({ scope, scopeRoot, env = process.env, fsImpl = fs }
|
|
187
|
+
function storageForgetScope({ scope, scopeRoot, env = process.env, fsImpl = fs } = {}) {
|
|
188
188
|
const exitCode = process.exitCode;
|
|
189
189
|
let removed = 0;
|
|
190
|
-
const failures
|
|
191
|
-
const attempt = (file
|
|
192
|
-
try { fn(); } catch (error
|
|
190
|
+
const failures = [];
|
|
191
|
+
const attempt = (file , fn ) => {
|
|
192
|
+
try { fn(); } catch (error ) { if (error.code !== 'ENOENT') failures.push(`${file}: ${error.code || error.message}`); }
|
|
193
193
|
};
|
|
194
|
-
const removeFile = (file
|
|
194
|
+
const removeFile = (file , exact = false) => attempt(file, () => {
|
|
195
195
|
const info = stat(file, fsImpl);
|
|
196
196
|
if (!info?.isFile() || info.isSymbolicLink()) return;
|
|
197
197
|
if (exact && fsImpl.readFileSync(file, 'utf8') !== ignoreBytes()) return;
|
|
198
198
|
fsImpl.unlinkSync(file); removed++;
|
|
199
199
|
});
|
|
200
|
-
const removeControls = (root
|
|
200
|
+
const removeControls = (root , ignore = true) => {
|
|
201
201
|
if (!usableRoot(root, fsImpl)) return;
|
|
202
202
|
if (ignore) removeFile(path.join(root, '.gitignore'), true);
|
|
203
203
|
removeFile(path.join(root, 'upkeep-due'));
|
|
@@ -235,7 +235,7 @@ function storageForgetScope({ scope, scopeRoot, env = process.env, fsImpl = fs }
|
|
|
235
235
|
}
|
|
236
236
|
if (failures.length) for (const failure of failures) console.error(`[WARN] Storage: ${failure}`);
|
|
237
237
|
else if (removed > 0) console.log(`[INFO] Storage: removed ${removed} DoFlow control file(s)`);
|
|
238
|
-
} catch (error
|
|
238
|
+
} catch (error ) {
|
|
239
239
|
try { console.error(`[WARN] Storage: ${error.code || error.message}`); } catch { /* no exit change */ }
|
|
240
240
|
} finally { process.exitCode = exitCode; }
|
|
241
241
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const { REPO_ROOT } = require('../../helper/repo-root.
|
|
8
|
-
const { loadStorageRegistry } = require('./registry.
|
|
7
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
8
|
+
const { loadStorageRegistry } = require('./registry.js');
|
|
9
9
|
|
|
10
10
|
const RUN_STATES = new Set(['RUNNING', 'AWAITING_GATE', 'COMPLETED', 'REJECTED']);
|
|
11
11
|
const RUN_OPEN_STATES = Object.freeze(['RUNNING', 'AWAITING_GATE']);
|
|
@@ -13,18 +13,18 @@ const UNCHECKED = /^\s*- \[ \] /m;
|
|
|
13
13
|
const UNCHECKED_TASK = /^\s*- \[ \] (\S+)/gm;
|
|
14
14
|
const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
15
|
|
|
16
|
-
function recordStorePaths(fsImpl
|
|
16
|
+
function recordStorePaths(fsImpl ) {
|
|
17
17
|
try {
|
|
18
18
|
const registry = loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl });
|
|
19
19
|
return new Map(registry.stores
|
|
20
|
-
.filter((store
|
|
21
|
-
.map((store
|
|
20
|
+
.filter((store ) => store.root === 'project-state' && store.entry === 'record' && store.rule === 'work')
|
|
21
|
+
.map((store ) => [store.id, store.path]));
|
|
22
22
|
} catch {
|
|
23
23
|
return new Map();
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function createWorkCache({ projectRoot, fsImpl = fs }
|
|
27
|
+
function createWorkCache({ projectRoot, fsImpl = fs } ) {
|
|
28
28
|
return {
|
|
29
29
|
projectRoot: path.resolve(projectRoot),
|
|
30
30
|
fsImpl,
|
|
@@ -43,38 +43,38 @@ function createWorkCache({ projectRoot, fsImpl = fs }: { projectRoot: string; fs
|
|
|
43
43
|
* plan and listing, so the recheck reads the work's current run and plan rather than the snapshot
|
|
44
44
|
* taken at selection. A run reopened or a plan task unchecked after selection keeps the record.
|
|
45
45
|
*/
|
|
46
|
-
function refreshedWorkCache(cache
|
|
46
|
+
function refreshedWorkCache(cache ) {
|
|
47
47
|
return { ...cache, runNames: null, runListError: false, featureNames: null, featureListError: false,
|
|
48
48
|
runs: new Map(), plans: new Map() };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function cacheFor({ projectRoot, fsImpl, cache }
|
|
51
|
+
function cacheFor({ projectRoot, fsImpl, cache } ) {
|
|
52
52
|
const resolved = path.resolve(projectRoot);
|
|
53
53
|
if (cache && cache.projectRoot === resolved && cache.fsImpl === fsImpl
|
|
54
54
|
&& cache.recordStorePaths instanceof Map && cache.runs instanceof Map && cache.plans instanceof Map) return cache;
|
|
55
55
|
return createWorkCache({ projectRoot: resolved, fsImpl });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function stateRoot(projectRoot
|
|
58
|
+
function stateRoot(projectRoot ) {
|
|
59
59
|
return path.join(projectRoot, '.doflow', 'state');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function safeSegment(value
|
|
62
|
+
function safeSegment(value ) {
|
|
63
63
|
return typeof value === 'string' && SAFE_SEGMENT.test(value) && !value.includes('..');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function listRunNames(cache
|
|
66
|
+
function listRunNames(cache , shouldStop ) {
|
|
67
67
|
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
68
68
|
if (cache.runNames !== null) return { names: cache.runNames, incomplete: false };
|
|
69
69
|
const dir = path.join(stateRoot(cache.projectRoot), 'orchestration');
|
|
70
|
-
let entries
|
|
70
|
+
let entries ;
|
|
71
71
|
try { entries = cache.fsImpl.readdirSync(dir, { withFileTypes: true }); }
|
|
72
|
-
catch (error
|
|
72
|
+
catch (error ) {
|
|
73
73
|
if (error?.code === 'ENOENT') entries = [];
|
|
74
74
|
else { cache.runNames = []; cache.runListError = true; return { names: cache.runNames, incomplete: false }; }
|
|
75
75
|
}
|
|
76
76
|
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
77
|
-
const names
|
|
77
|
+
const names = [];
|
|
78
78
|
for (const entry of entries) {
|
|
79
79
|
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
80
80
|
if (entry.name.endsWith('.json')) names.push(entry.name.slice(0, -5));
|
|
@@ -84,10 +84,10 @@ function listRunNames(cache: any, shouldStop?: () => boolean): { names: string[]
|
|
|
84
84
|
return { names, incomplete: false };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function readRun(cache
|
|
87
|
+
function readRun(cache , runId ) {
|
|
88
88
|
if (cache.runs.has(runId)) return cache.runs.get(runId);
|
|
89
89
|
const file = path.join(stateRoot(cache.projectRoot), 'orchestration', `${runId}.json`);
|
|
90
|
-
let result
|
|
90
|
+
let result ;
|
|
91
91
|
try {
|
|
92
92
|
const value = JSON.parse(cache.fsImpl.readFileSync(file, 'utf8'));
|
|
93
93
|
result = value && typeof value === 'object' && !Array.isArray(value) && RUN_STATES.has(value.state)
|
|
@@ -100,11 +100,11 @@ function readRun(cache: any, runId: string): any {
|
|
|
100
100
|
return result;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function resolveRun(cache
|
|
103
|
+
function resolveRun(cache , taskId , namespace , shouldStop ) {
|
|
104
104
|
const listed = listRunNames(cache, shouldStop);
|
|
105
105
|
if (listed.incomplete) return { id: null, unreadable: false, incomplete: true };
|
|
106
106
|
if (cache.runListError) return { id: null, unreadable: true };
|
|
107
|
-
let exact = false, namespaced = false, prefix
|
|
107
|
+
let exact = false, namespaced = false, prefix = null;
|
|
108
108
|
for (const candidate of listed.names) {
|
|
109
109
|
if (shouldStop?.()) return { id: null, unreadable: false, incomplete: true };
|
|
110
110
|
if (candidate === taskId) exact = true;
|
|
@@ -120,9 +120,9 @@ function resolveRun(cache: any, taskId: string, namespace: any, shouldStop?: ()
|
|
|
120
120
|
return read.ok ? { id, value: read.value, unreadable: false } : { id, value: null, unreadable: true };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function readRecord({ projectRoot, storePath, recordId, namespace, mtimeMs, fsImpl }
|
|
123
|
+
function readRecord({ projectRoot, storePath, recordId, namespace, mtimeMs, fsImpl } ) {
|
|
124
124
|
const storeSegments = typeof storePath === 'string' && storePath !== '' ? storePath.split('/') : [];
|
|
125
|
-
if (typeof storePath !== 'string' || storeSegments.some((segment
|
|
125
|
+
if (typeof storePath !== 'string' || storeSegments.some((segment ) => !safeSegment(segment))
|
|
126
126
|
|| !safeSegment(recordId)
|
|
127
127
|
|| (namespace !== null && namespace !== undefined && !safeSegment(namespace))) {
|
|
128
128
|
return { ok: false };
|
|
@@ -138,18 +138,18 @@ function readRecord({ projectRoot, storePath, recordId, namespace, mtimeMs, fsIm
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function listFeatureNames(cache
|
|
141
|
+
function listFeatureNames(cache , shouldStop ) {
|
|
142
142
|
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
143
143
|
if (cache.featureNames !== null) return { names: cache.featureNames, incomplete: false };
|
|
144
144
|
const dir = path.join(cache.projectRoot, 'agent-docs', 'doflow');
|
|
145
|
-
let entries
|
|
145
|
+
let entries ;
|
|
146
146
|
try { entries = cache.fsImpl.readdirSync(dir, { withFileTypes: true }); }
|
|
147
|
-
catch (error
|
|
147
|
+
catch (error ) {
|
|
148
148
|
if (error?.code === 'ENOENT') entries = [];
|
|
149
149
|
else { cache.featureNames = []; cache.featureListError = true; return { names: cache.featureNames, incomplete: false }; }
|
|
150
150
|
}
|
|
151
151
|
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
152
|
-
const names
|
|
152
|
+
const names = [];
|
|
153
153
|
for (const entry of entries) {
|
|
154
154
|
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
155
155
|
if (entry.isDirectory() && safeSegment(entry.name)) names.push(entry.name);
|
|
@@ -159,10 +159,10 @@ function listFeatureNames(cache: any, shouldStop?: () => boolean): { names: stri
|
|
|
159
159
|
return { names, incomplete: false };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function featureFromFolders(cache
|
|
162
|
+
function featureFromFolders(cache , taskId , shouldStop ) {
|
|
163
163
|
const listed = listFeatureNames(cache, shouldStop);
|
|
164
164
|
if (listed.incomplete) return { feature: null, incomplete: true };
|
|
165
|
-
let best
|
|
165
|
+
let best = null;
|
|
166
166
|
for (const name of listed.names) {
|
|
167
167
|
if (shouldStop?.()) return { feature: null, incomplete: true };
|
|
168
168
|
if ((taskId === name || taskId.startsWith(`${name}-`) || taskId.startsWith(`${name}.`))
|
|
@@ -171,18 +171,18 @@ function featureFromFolders(cache: any, taskId: string, shouldStop?: () => boole
|
|
|
171
171
|
return { feature: best, incomplete: false };
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
function stringField(record
|
|
174
|
+
function stringField(record , field ) {
|
|
175
175
|
const value = record && typeof record === 'object' ? record[field] : null;
|
|
176
176
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/** true, false when the folder is definitely absent, or null when it cannot be told. */
|
|
180
|
-
function folderExists(fsImpl
|
|
180
|
+
function folderExists(fsImpl , folder ) {
|
|
181
181
|
try { fsImpl.lstatSync(folder); return true; }
|
|
182
|
-
catch (error
|
|
182
|
+
catch (error ) { return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? false : null; }
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
function readPlan(cache
|
|
185
|
+
function readPlan(cache , feature , shouldStop ) {
|
|
186
186
|
if (shouldStop?.()) return { exists: false, ok: false, text: null, incomplete: true };
|
|
187
187
|
if (cache.plans.has(feature)) return cache.plans.get(feature);
|
|
188
188
|
if (!safeSegment(feature)) {
|
|
@@ -191,10 +191,10 @@ function readPlan(cache: any, feature: string, shouldStop?: () => boolean): any
|
|
|
191
191
|
return invalid;
|
|
192
192
|
}
|
|
193
193
|
const folder = path.join(cache.projectRoot, 'agent-docs', 'doflow', feature);
|
|
194
|
-
let result
|
|
194
|
+
let result ;
|
|
195
195
|
try {
|
|
196
196
|
result = { exists: true, ok: true, text: cache.fsImpl.readFileSync(path.join(folder, 'plan.md'), 'utf8'), folder: true };
|
|
197
|
-
} catch (error
|
|
197
|
+
} catch (error ) {
|
|
198
198
|
result = error?.code === 'ENOENT'
|
|
199
199
|
? { exists: false, ok: true, text: null, folder: folderExists(cache.fsImpl, folder) }
|
|
200
200
|
: { exists: true, ok: false, text: null };
|
|
@@ -205,7 +205,7 @@ function readPlan(cache: any, feature: string, shouldStop?: () => boolean): any
|
|
|
205
205
|
return result;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
function planWithUncheckedTask(cache
|
|
208
|
+
function planWithUncheckedTask(cache , taskId , shouldStop ) {
|
|
209
209
|
const listed = listFeatureNames(cache, shouldStop);
|
|
210
210
|
if (listed.incomplete) return { found: false, unreadable: false, incomplete: true };
|
|
211
211
|
if (cache.featureListError) return { found: false, unreadable: true };
|
|
@@ -216,7 +216,7 @@ function planWithUncheckedTask(cache: any, taskId: string, shouldStop?: () => bo
|
|
|
216
216
|
if (!plan.ok) return { found: false, unreadable: true };
|
|
217
217
|
if (!plan.exists) continue;
|
|
218
218
|
UNCHECKED_TASK.lastIndex = 0;
|
|
219
|
-
let match
|
|
219
|
+
let match ;
|
|
220
220
|
while ((match = UNCHECKED_TASK.exec(plan.text)) !== null) {
|
|
221
221
|
if (shouldStop?.()) return { found: false, unreadable: false, incomplete: true };
|
|
222
222
|
if (match[1] === taskId) return { found: true, unreadable: false };
|
|
@@ -226,11 +226,11 @@ function planWithUncheckedTask(cache: any, taskId: string, shouldStop?: () => bo
|
|
|
226
226
|
return { found: false, unreadable: false };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
function answer(state
|
|
229
|
+
function answer(state , reason , run , feature , since = null) {
|
|
230
230
|
return { state, since, reason, run, feature };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
function incompleteAnswer(run
|
|
233
|
+
function incompleteAnswer(run = null, feature = null) {
|
|
234
234
|
return { ...answer('undecidable', 'pass-budget-exhausted', run, feature), incomplete: true };
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -242,7 +242,7 @@ function incompleteAnswer(run: any = null, feature: any = null): any {
|
|
|
242
242
|
*/
|
|
243
243
|
function workStatus({
|
|
244
244
|
projectRoot, store, taskId, namespace = null, mtimeMs, cache, fsImpl, shouldStop,
|
|
245
|
-
}
|
|
245
|
+
} ) {
|
|
246
246
|
const project = path.resolve(projectRoot);
|
|
247
247
|
const fileSystem = fsImpl || cache?.fsImpl || fs;
|
|
248
248
|
const pass = cacheFor({ projectRoot: project, fsImpl: fileSystem, cache });
|
|
@@ -262,7 +262,7 @@ function workStatus({
|
|
|
262
262
|
// path. An orchestration record is also its exact run file, so reuse the cached parse rather than
|
|
263
263
|
// reading that file twice.
|
|
264
264
|
const storePath = pass.recordStorePaths.get(store);
|
|
265
|
-
let record
|
|
265
|
+
let record ;
|
|
266
266
|
if (storePath === 'orchestration' && !namespace && recordId === workId && resolvedRun.id === workId && run) {
|
|
267
267
|
try {
|
|
268
268
|
const file = path.join(stateRoot(project), storePath, `${recordId}.json`);
|
|
@@ -285,7 +285,7 @@ function workStatus({
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
// W5: feature metadata is ordered; folder-name inference is the final fallback.
|
|
288
|
-
let feature
|
|
288
|
+
let feature = stringField(run, 'featureSlug')
|
|
289
289
|
|| stringField(record.value, 'featureSlug')
|
|
290
290
|
|| stringField(record.value, 'slug')
|
|
291
291
|
|| (safeSegment(namespace) ? namespace : null);
|