@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
|
@@ -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);
|