@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
|
@@ -52,69 +52,69 @@ const SANDBOX_SKILLS_DIR = path.join('.claude', 'skills');
|
|
|
52
52
|
/** Source of truth for skill content, relative to the repo root. */
|
|
53
53
|
const SOURCE_SKILLS_DIR = path.join('core', 'shared', 'skills');
|
|
54
54
|
|
|
55
|
-
function sha256File(file
|
|
55
|
+
function sha256File(file ) {
|
|
56
56
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function git(args
|
|
59
|
+
function git(args , cwd , { allowFailure = false } = {}) {
|
|
60
60
|
try {
|
|
61
61
|
return {
|
|
62
62
|
ok: true,
|
|
63
63
|
stdout: execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }),
|
|
64
64
|
};
|
|
65
|
-
} catch (err
|
|
65
|
+
} catch (err ) {
|
|
66
66
|
if (!allowFailure) throw err;
|
|
67
67
|
return { ok: false, stdout: '', stderr: err.stderr ? String(err.stderr) : String(err.message) };
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
class WorktreeManager {
|
|
72
|
-
repoRoot
|
|
73
|
-
worktreesDir
|
|
72
|
+
repoRoot ;
|
|
73
|
+
worktreesDir ;
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* @param {string} repoRoot repository whose worktrees are managed
|
|
77
77
|
* @param {string} [worktreesDir] override the sandbox root; defaults to `<repo>/.doflow/worktrees`,
|
|
78
78
|
* which is gitignored so sandboxes never appear as untracked noise in the parent tree.
|
|
79
79
|
*/
|
|
80
|
-
constructor(repoRoot
|
|
80
|
+
constructor(repoRoot , worktreesDir ) {
|
|
81
81
|
this.repoRoot = repoRoot;
|
|
82
82
|
this.worktreesDir = worktreesDir || path.join(repoRoot, '.doflow', 'worktrees');
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
static assertSafeId(taskId
|
|
85
|
+
static assertSafeId(taskId ) {
|
|
86
86
|
if (!taskId || !SAFE_ID.test(taskId)) {
|
|
87
87
|
throw new Error(`unsafe worktree id ${JSON.stringify(taskId)} — allowed: letters, digits, dot, dash, underscore`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
pathFor(taskId
|
|
91
|
+
pathFor(taskId ) {
|
|
92
92
|
WorktreeManager.assertSafeId(taskId);
|
|
93
93
|
return path.join(this.worktreesDir, taskId);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
branchFor(taskId
|
|
96
|
+
branchFor(taskId ) {
|
|
97
97
|
WorktreeManager.assertSafeId(taskId);
|
|
98
98
|
return `task/${taskId}`;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** Whether git itself considers this path a registered worktree — the directory existing on disk
|
|
102
102
|
* is not the same question, which is the bug fixed here. */
|
|
103
|
-
isRegistered(wtPath
|
|
103
|
+
isRegistered(wtPath ) {
|
|
104
104
|
const res = git(['worktree', 'list', '--porcelain'], this.repoRoot, { allowFailure: true });
|
|
105
105
|
if (!res.ok) return false;
|
|
106
106
|
const resolved = path.resolve(wtPath);
|
|
107
107
|
return res.stdout
|
|
108
108
|
.split('\n')
|
|
109
|
-
.filter((l
|
|
110
|
-
.some((l
|
|
109
|
+
.filter((l ) => l.startsWith('worktree '))
|
|
110
|
+
.some((l ) => path.resolve(l.slice('worktree '.length).trim()) === resolved);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Create an isolated worktree, reusing it only when git agrees it is registered. Returns the
|
|
115
115
|
* absolute path.
|
|
116
116
|
*/
|
|
117
|
-
create(taskId
|
|
117
|
+
create(taskId , baseRef = 'HEAD') {
|
|
118
118
|
const wtPath = this.pathFor(taskId);
|
|
119
119
|
const branch = this.branchFor(taskId);
|
|
120
120
|
|
|
@@ -142,7 +142,7 @@ class WorktreeManager {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/** The resolved base SHA recorded at creation, or null when unavailable. */
|
|
145
|
-
baseOf(taskId
|
|
145
|
+
baseOf(taskId ) {
|
|
146
146
|
const file = path.join(this.pathFor(taskId), BASE_FILE);
|
|
147
147
|
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() : null;
|
|
148
148
|
}
|
|
@@ -172,7 +172,7 @@ class WorktreeManager {
|
|
|
172
172
|
*
|
|
173
173
|
* @returns {object} the manifest also written to `<worktree>/.doflow-skill-source.json`
|
|
174
174
|
*/
|
|
175
|
-
projectSkills(taskId
|
|
175
|
+
projectSkills(taskId , { harness = 'claude', cli = path.join('bin', 'doflow.js'), skillsRoot = SOURCE_SKILLS_DIR } = {}) {
|
|
176
176
|
const wtPath = this.pathFor(taskId);
|
|
177
177
|
if (!fs.existsSync(wtPath)) {
|
|
178
178
|
throw new Error(`no worktree at ${wtPath} — create it before projecting skills`);
|
|
@@ -182,25 +182,25 @@ class WorktreeManager {
|
|
|
182
182
|
throw new Error(`no skill source at ${sourceRoot}`);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
let installOutput
|
|
185
|
+
let installOutput ;
|
|
186
186
|
try {
|
|
187
187
|
installOutput = execFileSync(
|
|
188
188
|
process.execPath,
|
|
189
189
|
[path.join(this.repoRoot, cli), 'install', wtPath, '--force', '-t', harness],
|
|
190
190
|
{ cwd: this.repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
|
|
191
191
|
);
|
|
192
|
-
} catch (err
|
|
192
|
+
} catch (err ) {
|
|
193
193
|
throw new Error(`projecting skills into ${wtPath} failed: ${err.stderr || err.message}`);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const names = fs
|
|
197
197
|
.readdirSync(sourceRoot, { withFileTypes: true })
|
|
198
|
-
.filter((e
|
|
199
|
-
.map((e
|
|
198
|
+
.filter((e ) => e.isDirectory() && fs.existsSync(path.join(sourceRoot, e.name, 'SKILL.md')))
|
|
199
|
+
.map((e ) => e.name)
|
|
200
200
|
.sort();
|
|
201
201
|
|
|
202
|
-
const skills
|
|
203
|
-
const mismatches
|
|
202
|
+
const skills = {};
|
|
203
|
+
const mismatches = [];
|
|
204
204
|
for (const name of names) {
|
|
205
205
|
const rel = path.join(SANDBOX_SKILLS_DIR, name, 'SKILL.md');
|
|
206
206
|
const dst = path.join(wtPath, rel);
|
|
@@ -237,7 +237,7 @@ class WorktreeManager {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/** The skill-source manifest a sandbox recorded, or null when it was never projected. */
|
|
240
|
-
skillSourceOf(taskId
|
|
240
|
+
skillSourceOf(taskId ) {
|
|
241
241
|
const file = path.join(this.pathFor(taskId), SKILL_SOURCE_FILE);
|
|
242
242
|
if (!fs.existsSync(file)) return null;
|
|
243
243
|
try {
|
|
@@ -249,7 +249,7 @@ class WorktreeManager {
|
|
|
249
249
|
|
|
250
250
|
/** Repo HEAD, or null outside a repository. Recorded for readability; the per-skill hashes are
|
|
251
251
|
* the actual provenance, and they stay correct on a dirty tree where a commit id does not. */
|
|
252
|
-
headCommit()
|
|
252
|
+
headCommit() {
|
|
253
253
|
const res = git(['rev-parse', 'HEAD'], this.repoRoot, { allowFailure: true });
|
|
254
254
|
return res.ok ? res.stdout.trim() : null;
|
|
255
255
|
}
|
|
@@ -259,13 +259,13 @@ class WorktreeManager {
|
|
|
259
259
|
* skills. Split from `create()` because non-evaluation callers want a bare worktree and should
|
|
260
260
|
* not pay for a projection they will not read.
|
|
261
261
|
*/
|
|
262
|
-
createSandbox(taskId
|
|
262
|
+
createSandbox(taskId , { baseRef = 'HEAD', ...opts } = {}) {
|
|
263
263
|
const wtPath = this.create(taskId, baseRef);
|
|
264
264
|
return { path: wtPath, manifest: this.projectSkills(taskId, opts) };
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/** Remove the worktree and its branch. Idempotent: removing something absent is success. */
|
|
268
|
-
remove(taskId
|
|
268
|
+
remove(taskId ) {
|
|
269
269
|
const wtPath = this.pathFor(taskId);
|
|
270
270
|
const branch = this.branchFor(taskId);
|
|
271
271
|
|
|
@@ -282,7 +282,7 @@ class WorktreeManager {
|
|
|
282
282
|
* collapsing a failure into an empty diff, so a caller can tell "nothing changed" from
|
|
283
283
|
* "could not tell", which the Python version could not.
|
|
284
284
|
*/
|
|
285
|
-
collectDiff(taskId
|
|
285
|
+
collectDiff(taskId , baseRef ) {
|
|
286
286
|
const wtPath = this.pathFor(taskId);
|
|
287
287
|
if (!fs.existsSync(wtPath)) {
|
|
288
288
|
return { diff: null, untracked: [], error: `no worktree at ${wtPath}` };
|
|
@@ -304,23 +304,23 @@ class WorktreeManager {
|
|
|
304
304
|
// Harness bookkeeping is not task work: the base pointer and the skill-source manifest are
|
|
305
305
|
// written by this module, not by the skill under test.
|
|
306
306
|
untracked: untracked.ok
|
|
307
|
-
? untracked.stdout.split('\n').filter((f
|
|
307
|
+
? untracked.stdout.split('\n').filter((f ) => f && f !== BASE_FILE && f !== SKILL_SOURCE_FILE)
|
|
308
308
|
: [],
|
|
309
309
|
error: null,
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/** Every sandbox this manager knows about — used to clean up after an interrupted run. */
|
|
314
|
-
list()
|
|
314
|
+
list() {
|
|
315
315
|
if (!fs.existsSync(this.worktreesDir)) return [];
|
|
316
316
|
return fs
|
|
317
317
|
.readdirSync(this.worktreesDir, { withFileTypes: true })
|
|
318
|
-
.filter((e
|
|
319
|
-
.map((e
|
|
318
|
+
.filter((e ) => e.isDirectory())
|
|
319
|
+
.map((e ) => e.name);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
/** Run `fn(worktreePath)` in a fresh sandbox and always tear it down, even when `fn` throws. */
|
|
323
|
-
withWorktree(taskId
|
|
323
|
+
withWorktree(taskId , fn , baseRef = 'HEAD') {
|
|
324
324
|
const wtPath = this.create(taskId, baseRef);
|
|
325
325
|
try {
|
|
326
326
|
return fn(wtPath);
|
|
@@ -9,7 +9,7 @@ const fs = require('node:fs');
|
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const crypto = require('node:crypto');
|
|
12
|
-
const { manifestPath, readManifest } = require('../install/manifest.
|
|
12
|
+
const { manifestPath, readManifest } = require('../install/manifest.js');
|
|
13
13
|
|
|
14
14
|
const STATE_VERSION = 1;
|
|
15
15
|
// Ledger document version. v1 ledgers (no tombstones) remain readable forever; writers produce
|
|
@@ -18,32 +18,32 @@ const LEDGER_VERSION = 2;
|
|
|
18
18
|
const LEDGER_FILE = 'ledger.json';
|
|
19
19
|
const RECOVERY_DIR = 'recovery';
|
|
20
20
|
|
|
21
|
-
function assertScope(scope
|
|
21
|
+
function assertScope(scope ) {
|
|
22
22
|
if (scope !== 'project' && scope !== 'global') throw new Error(`Invalid state scope: '${scope}'`);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Resolve DoFlow-owned state without deriving it from a harness directory. */
|
|
26
|
-
function stateRoot({ scope, projectRoot = '.', homeDir = os.homedir() }
|
|
26
|
+
function stateRoot({ scope, projectRoot = '.', homeDir = os.homedir() } ) {
|
|
27
27
|
assertScope(scope);
|
|
28
28
|
return scope === 'project'
|
|
29
29
|
? path.join(path.resolve(projectRoot), '.doflow', 'state')
|
|
30
30
|
: path.join(path.resolve(homeDir), '.doflow', 'state');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function ledgerPath(root
|
|
34
|
-
function recoveryRoot(root
|
|
33
|
+
function ledgerPath(root ) { return path.join(root, LEDGER_FILE); }
|
|
34
|
+
function recoveryRoot(root ) { return path.join(root, RECOVERY_DIR); }
|
|
35
35
|
|
|
36
|
-
function stable(value
|
|
36
|
+
function stable(value ) {
|
|
37
37
|
if (Array.isArray(value)) return value.map(stable);
|
|
38
38
|
if (!value || typeof value !== 'object') return value;
|
|
39
39
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function fingerprint(value
|
|
42
|
+
function fingerprint(value ) {
|
|
43
43
|
return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function atomicJsonWrite(file
|
|
46
|
+
function atomicJsonWrite(file , value , { fsImpl = fs } = {}) {
|
|
47
47
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
48
48
|
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
49
49
|
try {
|
|
@@ -55,7 +55,7 @@ function atomicJsonWrite(file: string, value: any, { fsImpl = fs }: { fsImpl?: a
|
|
|
55
55
|
return file;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function defaultLedger({ scope, scopeRoot }
|
|
58
|
+
function defaultLedger({ scope, scopeRoot } ) {
|
|
59
59
|
assertScope(scope);
|
|
60
60
|
return {
|
|
61
61
|
version: LEDGER_VERSION,
|
|
@@ -72,13 +72,13 @@ function defaultLedger({ scope, scopeRoot }: { scope: any, scopeRoot: string }):
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** Upgrade a v1 ledger in memory to the current shape without touching disk. Idempotent. */
|
|
75
|
-
function upgradeLedger(ledger
|
|
75
|
+
function upgradeLedger(ledger ) {
|
|
76
76
|
if (!ledger || typeof ledger !== 'object') throw new Error('Invalid neutral ledger: expected object');
|
|
77
77
|
if (ledger.version === LEDGER_VERSION) return ledger;
|
|
78
78
|
return { ...ledger, version: LEDGER_VERSION, tombstones: Array.isArray(ledger.tombstones) ? ledger.tombstones : [] };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function validateLedger(value
|
|
81
|
+
function validateLedger(value ) {
|
|
82
82
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid neutral ledger: expected object');
|
|
83
83
|
if (![STATE_VERSION, LEDGER_VERSION].includes(value.version)) throw new Error(`Unsupported neutral ledger version: '${value.version}'`);
|
|
84
84
|
if (value.version >= LEDGER_VERSION && value.tombstones !== undefined && !Array.isArray(value.tombstones)) {
|
|
@@ -91,30 +91,30 @@ function validateLedger(value: any): any {
|
|
|
91
91
|
return value;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function readLedger(root
|
|
94
|
+
function readLedger(root , { fsImpl = fs } = {}) {
|
|
95
95
|
const file = ledgerPath(root);
|
|
96
96
|
if (!fsImpl.existsSync(file)) return null;
|
|
97
97
|
try { return validateLedger(JSON.parse(fsImpl.readFileSync(file, 'utf8'))); }
|
|
98
|
-
catch (error
|
|
98
|
+
catch (error ) { throw new Error(`Cannot read neutral ledger '${file}': ${error.message}`); }
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function writeLedger(root
|
|
101
|
+
function writeLedger(root , ledger , options = {}) {
|
|
102
102
|
return atomicJsonWrite(ledgerPath(root), validateLedger(ledger), options);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function recoveryId(now
|
|
105
|
+
function recoveryId(now = new Date()) {
|
|
106
106
|
return `recovery_${now.toISOString().replace(/[:.]/g, '-').replace(/Z$/, 'Z')}_${crypto.randomBytes(4).toString('hex')}`;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function assertSafeRecoveryId(id
|
|
109
|
+
function assertSafeRecoveryId(id ) {
|
|
110
110
|
if (typeof id !== 'string' || !/^recovery_[A-Za-z0-9_-]+$/.test(id)) throw new Error(`Invalid recovery id: '${id}'`);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** Persist a journal record before native mutation; append-only records make interrupted runs observable. */
|
|
114
|
-
function writeRecoveryRecord(root
|
|
114
|
+
function writeRecoveryRecord(root , record , { now = new Date(), fsImpl = fs } = {}) {
|
|
115
115
|
const id = record?.id ?? recoveryId(now);
|
|
116
116
|
assertSafeRecoveryId(id);
|
|
117
|
-
const value
|
|
117
|
+
const value = {
|
|
118
118
|
id,
|
|
119
119
|
version: STATE_VERSION,
|
|
120
120
|
status: record?.status ?? 'pending',
|
|
@@ -126,19 +126,19 @@ function writeRecoveryRecord(root: string, record: any, { now = new Date(), fsIm
|
|
|
126
126
|
return { id, file: atomicJsonWrite(path.join(recoveryRoot(root), `${id}.json`), value, { fsImpl }), record: value };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function readRecoveryRecord(root
|
|
129
|
+
function readRecoveryRecord(root , id , { fsImpl = fs } = {}) {
|
|
130
130
|
assertSafeRecoveryId(id);
|
|
131
131
|
const file = path.join(recoveryRoot(root), `${id}.json`);
|
|
132
132
|
if (!fsImpl.existsSync(file)) return null;
|
|
133
133
|
try { return JSON.parse(fsImpl.readFileSync(file, 'utf8')); }
|
|
134
|
-
catch (error
|
|
134
|
+
catch (error ) { throw new Error(`Cannot read recovery record '${file}': ${error.message}`); }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function ownershipKey(record
|
|
137
|
+
function ownershipKey(record ) {
|
|
138
138
|
return [record.harness, record.scope, record.assetId, record.target, record.ownershipIdentity].join('\u0000');
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function importLegacyResource(resource
|
|
141
|
+
function importLegacyResource(resource , scope ) {
|
|
142
142
|
if (!resource || typeof resource !== 'object') return null;
|
|
143
143
|
if (typeof resource.target !== 'string' || typeof resource.kind !== 'string' || typeof resource.identity !== 'string') return null;
|
|
144
144
|
return {
|
|
@@ -159,24 +159,24 @@ function importLegacyResource(resource: any, scope: any): any {
|
|
|
159
159
|
* Read-compatible, conservative legacy import. Only explicit `managed_resources` records are
|
|
160
160
|
* converted to ownership claims; a legacy tool entry alone never authorizes adoption of files.
|
|
161
161
|
*/
|
|
162
|
-
function migrateLegacyManifest({ root, scope, scopeRoot, claudeDir, now = new Date(), fsImpl = fs }
|
|
162
|
+
function migrateLegacyManifest({ root, scope, scopeRoot, claudeDir, now = new Date(), fsImpl = fs } ) {
|
|
163
163
|
assertScope(scope);
|
|
164
164
|
const legacyFile = manifestPath(claudeDir);
|
|
165
165
|
const current = readLedger(root, { fsImpl }) ?? defaultLedger({ scope, scopeRoot });
|
|
166
166
|
if (current.scope !== scope || current.scopeRoot !== path.resolve(scopeRoot)) throw new Error('Neutral ledger scope does not match requested migration scope');
|
|
167
|
-
if (current.legacyImports.some((item
|
|
167
|
+
if (current.legacyImports.some((item ) => item.path === legacyFile)) return { migrated: false, reason: 'already-imported', ledger: current, legacyFile };
|
|
168
168
|
if (!fsImpl.existsSync(legacyFile)) return { migrated: false, reason: 'legacy-missing', ledger: current, legacyFile };
|
|
169
169
|
|
|
170
170
|
const legacy = readManifest(claudeDir);
|
|
171
171
|
if (!legacy || legacy.operation === 'error') return { migrated: false, reason: 'legacy-invalid', ledger: current, legacyFile };
|
|
172
|
-
const resources = legacy.managedResources.map((resource
|
|
172
|
+
const resources = legacy.managedResources.map((resource ) => importLegacyResource(resource, scope)).filter(Boolean);
|
|
173
173
|
const existing = new Set(current.resources.map(ownershipKey));
|
|
174
174
|
for (const resource of resources) if (!existing.has(ownershipKey(resource))) current.resources.push(resource);
|
|
175
175
|
|
|
176
176
|
// The legacy manifest's MCP selection was owned by Claude's config flow. Do not infer an
|
|
177
177
|
// equivalent selection for Codex or Gemini merely because they happened to share the old file.
|
|
178
178
|
if (Array.isArray(legacy.mcpServers) && !Object.hasOwn(current.mcpSelections, 'claude')) current.mcpSelections.claude = [...legacy.mcpServers];
|
|
179
|
-
for (const [harness, status] of Object.entries(legacy.tools)
|
|
179
|
+
for (const [harness, status] of Object.entries(legacy.tools) ) {
|
|
180
180
|
if (status?.installed && !current.targets[harness]) current.targets[harness] = { installed: true, lastUpdated: status.last_updated ?? null, imported: true };
|
|
181
181
|
}
|
|
182
182
|
current.legacyImports.push({ path: legacyFile, importedAt: now.toISOString(), sourceFingerprint: fingerprint({ operation: legacy.operation, lastRun: legacy.lastRun, backupId: legacy.backupId, resources: legacy.managedResources }), lastBackupId: legacy.backupId });
|
|
@@ -10,25 +10,25 @@
|
|
|
10
10
|
const fs = require('node:fs');
|
|
11
11
|
const os = require('node:os');
|
|
12
12
|
const path = require('node:path');
|
|
13
|
-
const { atomicJsonWrite } = require('./index.
|
|
13
|
+
const { atomicJsonWrite } = require('./index.js');
|
|
14
14
|
|
|
15
15
|
const LOCK_VERSION = 1;
|
|
16
16
|
const LOCK_FILE = 'doflow.lock';
|
|
17
17
|
|
|
18
|
-
function lockRoot({ scope, projectRoot = '.', homeDir = os.homedir() }
|
|
18
|
+
function lockRoot({ scope, projectRoot = '.', homeDir = os.homedir() } ) {
|
|
19
19
|
if (scope !== 'project' && scope !== 'global') throw new Error(`Invalid lock scope: '${scope}'`);
|
|
20
20
|
return scope === 'project' ? path.resolve(projectRoot) : path.resolve(homeDir);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function lockPath(options
|
|
23
|
+
function lockPath(options ) { return path.join(lockRoot(options), '.doflow', LOCK_FILE); }
|
|
24
24
|
|
|
25
|
-
function stable(value
|
|
26
|
-
if (Array.isArray(value)) return [...value].map(stable).sort((a
|
|
25
|
+
function stable(value ) {
|
|
26
|
+
if (Array.isArray(value)) return [...value].map(stable).sort((a , b ) => JSON.stringify(a) < JSON.stringify(b) ? -1 : 1);
|
|
27
27
|
if (!value || typeof value !== 'object') return value;
|
|
28
28
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function defaultLock({ scope, scopeRoot }
|
|
31
|
+
function defaultLock({ scope, scopeRoot } ) {
|
|
32
32
|
if (scope !== 'project' && scope !== 'global') throw new Error(`Invalid lock scope: '${scope}'`);
|
|
33
33
|
return {
|
|
34
34
|
version: LOCK_VERSION,
|
|
@@ -42,7 +42,7 @@ function defaultLock({ scope, scopeRoot }: { scope: any, scopeRoot: string }): a
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function validateLock(value
|
|
45
|
+
function validateLock(value ) {
|
|
46
46
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid doflow.lock: expected object');
|
|
47
47
|
if (value.version !== LOCK_VERSION) throw new Error(`Unsupported doflow.lock version: '${value.version}'`);
|
|
48
48
|
if (value.scope !== 'project' && value.scope !== 'global') throw new Error(`Invalid doflow.lock scope: '${value.scope}'`);
|
|
@@ -54,21 +54,21 @@ function validateLock(value: any): any {
|
|
|
54
54
|
throw new Error('Invalid doflow.lock: mcpSelections must be an object');
|
|
55
55
|
}
|
|
56
56
|
for (const [harness, ids] of Object.entries(value.mcpSelections)) {
|
|
57
|
-
if (!Array.isArray(ids) || !ids.every((id
|
|
57
|
+
if (!Array.isArray(ids) || !ids.every((id ) => typeof id === 'string')) {
|
|
58
58
|
throw new Error(`Invalid doflow.lock: mcpSelections.${harness} must be an array of server ids`);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
return value;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function readLock(options
|
|
64
|
+
function readLock(options , { fsImpl = fs } = {}) {
|
|
65
65
|
const file = lockPath(options);
|
|
66
66
|
if (!fsImpl.existsSync(file)) return null;
|
|
67
67
|
try { return validateLock(JSON.parse(fsImpl.readFileSync(file, 'utf8'))); }
|
|
68
|
-
catch (error
|
|
68
|
+
catch (error ) { throw new Error(`Cannot read doflow.lock '${file}': ${error.message}`); }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function writeLock(options
|
|
71
|
+
function writeLock(options , lock , { fsImpl = fs } = {}) {
|
|
72
72
|
// Validate the caller's document first so a foreign version fails loudly here rather than
|
|
73
73
|
// being silently normalized into something no future reader can distinguish.
|
|
74
74
|
validateLock(lock);
|
|
@@ -77,23 +77,23 @@ function writeLock(options: any, lock: any, { fsImpl = fs }: { fsImpl?: any } =
|
|
|
77
77
|
|
|
78
78
|
/** The lock's MCP rows for the harnesses it targets. A row naming a harness outside `targets` is
|
|
79
79
|
* ignored by every reader; the next write drops it. */
|
|
80
|
-
function pinnedSelections(lock
|
|
81
|
-
const targets = new Set((lock?.targets ?? []).map((entry
|
|
80
|
+
function pinnedSelections(lock ) {
|
|
81
|
+
const targets = new Set((lock?.targets ?? []).map((entry ) => entry.harness));
|
|
82
82
|
return Object.fromEntries(Object.entries(lock?.mcpSelections ?? {}).filter(([harness]) => targets.has(harness)));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** Compare two lock documents. Arrays are keyed by identity (assets by id, targets by harness,
|
|
86
86
|
* fallback to full JSON), mcpSelections by harness, where an absent row (`null`) and a recorded
|
|
87
87
|
* empty selection (`[]`) differ. Returns {added, removed, changed} per section plus a clean flag. */
|
|
88
|
-
function diffLocks(previousLock
|
|
88
|
+
function diffLocks(previousLock , nextLock ) {
|
|
89
89
|
// Compare in the order writeLock stores, so a document built in memory never differs from the
|
|
90
90
|
// file it was written to.
|
|
91
91
|
const previous = previousLock && stable(previousLock);
|
|
92
92
|
const next = nextLock && stable(nextLock);
|
|
93
|
-
const key = (entry
|
|
94
|
-
const sectionDiff = (before
|
|
95
|
-
const beforeKeys = new Map
|
|
96
|
-
const afterKeys = new Map
|
|
93
|
+
const key = (entry ) => entry?.id ?? entry?.harness ?? JSON.stringify(entry);
|
|
94
|
+
const sectionDiff = (before , after ) => {
|
|
95
|
+
const beforeKeys = new Map ((before || []).map((entry ) => [key(entry), entry]));
|
|
96
|
+
const afterKeys = new Map ((after || []).map((entry ) => [key(entry), entry]));
|
|
97
97
|
const added = [...afterKeys].filter(([k]) => !beforeKeys.has(k)).map(([, v]) => v);
|
|
98
98
|
const removed = [...beforeKeys].filter(([k]) => !afterKeys.has(k)).map(([, v]) => v);
|
|
99
99
|
const changed = [...afterKeys]
|
|
@@ -107,7 +107,7 @@ function diffLocks(previousLock: any, nextLock: any): any {
|
|
|
107
107
|
const mcpChanged = [...harnesses]
|
|
108
108
|
.filter((harness) => JSON.stringify(selectionsBefore[harness] ?? null) !== JSON.stringify(selectionsAfter[harness] ?? null))
|
|
109
109
|
.map((harness) => ({ harness, before: selectionsBefore[harness] ?? null, after: selectionsAfter[harness] ?? null }));
|
|
110
|
-
const result
|
|
110
|
+
const result = {
|
|
111
111
|
targets: sectionDiff(previous?.targets, next?.targets),
|
|
112
112
|
assets: sectionDiff(previous?.assets, next?.assets),
|
|
113
113
|
mcpSelections: { changed: mcpChanged },
|
|
@@ -124,7 +124,7 @@ function diffLocks(previousLock: any, nextLock: any): any {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** Delete the lock when nothing remains pinned. Returns true when a file actually went away. */
|
|
127
|
-
function removeLock(options
|
|
127
|
+
function removeLock(options , { fsImpl = fs } = {}) {
|
|
128
128
|
const file = lockPath(options);
|
|
129
129
|
if (!fsImpl.existsSync(file)) return false;
|
|
130
130
|
fsImpl.rmSync(file);
|