iterate-plugin 2.9.2 → 2.9.3

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.
@@ -167,25 +167,46 @@ export function validateConfig(config) {
167
167
  * path is unsafe; callers must short-circuit on the failure and return a
168
168
  * structured error instead of proceeding.
169
169
  */
170
- export function resolveProjectRoot(input) {
170
+ /**
171
+ * Resolve a caller-supplied project root to a safe absolute path.
172
+ *
173
+ * Resolution order for the default (no explicit `path`) case:
174
+ * 1. `sessionCwd` — the absolute working directory the calling DSH session
175
+ * was created in (`exec.agent.session.header.cwd`). This is the
176
+ * authoritative workspace for the current conversation and is immune to
177
+ * where the web-service process happened to start.
178
+ * 2. the process cwd, when it is a usable directory (not `/` or the home
179
+ * dir — launchd/daemon-managed servers start with cwd=`/`);
180
+ * 3. the session workspace decoded from `DSH_SESSION_JSONL` (present when
181
+ * the DSH runtime injects per-session env into tool sub-processes).
182
+ */
183
+ export function resolveProjectRoot(input, sessionCwd) {
171
184
  const raw = (input ?? '').trim();
172
- const root = raw ? resolve(raw) : resolve(effectiveCwd());
185
+ const root = raw ? resolve(raw) : resolve(effectiveCwd(sessionCwd));
173
186
  if (!root || root === sep) {
174
187
  return { ok: false, reason: 'Refusing filesystem root as project root.' };
175
188
  }
176
189
  return { ok: true, root };
177
190
  }
191
+ /**
192
+ * Thin adapter for tool `execute(args, exec)` bodies: pull the session cwd
193
+ * from the DSH run context and hand it to {@link resolveProjectRoot}.
194
+ */
195
+ export function resolveProjectRootForExec(exec, input) {
196
+ return resolveProjectRoot(input, exec?.agent?.session?.header?.cwd);
197
+ }
178
198
  /**
179
199
  * Resolve the default working directory for tools invoked without an explicit
180
- * `path`. Prefers the process cwd, but a daemon-managed web server can start
181
- * with cwd = `/` (e.g. launchd), which is not a usable project root. In that
182
- * case fall back to the session workspace encoded in `DSH_SESSION_JSONL`
200
+ * `path`. Prefers the caller-provided session cwd, then the process cwd, then
201
+ * the session workspace encoded in `DSH_SESSION_JSONL`
183
202
  * (`…/sessions/<encoded-workspace>/<session-id>/session.jsonl.zstd`), where
184
203
  * the workspace directory is `--`-wrapped with `/` → `-` and percent-encoded
185
204
  * bytes spelled as `~<hex>` (e.g. `/Volumes/Eng-Dev/iterate-skill` →
186
205
  * `--Volumes-Eng-Dev-iterate-skill--`).
187
206
  */
188
- function effectiveCwd() {
207
+ function effectiveCwd(sessionCwd) {
208
+ if (sessionCwd && sessionCwd !== sep && sessionCwd !== homedir())
209
+ return sessionCwd;
189
210
  let cwd = '';
190
211
  try {
191
212
  cwd = process.cwd();
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
12
  import { defineTool } from '@deepseek-ai/dsh-tools';
13
- import { resolveProjectRoot } from "../config-loader.js";
13
+ import { resolveProjectRootForExec } from "../config-loader.js";
14
14
  import { checkpointPath, iterateDir } from "../paths.js";
15
15
  import { readRegistry } from "./fix.js";
16
16
  import { readDecisionEntries } from "./decision-log.js";
@@ -142,8 +142,8 @@ export function registerCheckpointTool(ctx) {
142
142
  { type: 'text', text: JSON.stringify(value, null, 2) },
143
143
  ],
144
144
  },
145
- async execute(args) {
146
- const resolved = resolveProjectRoot(args.path);
145
+ async execute(args, exec) {
146
+ const resolved = resolveProjectRootForExec(exec, args.path);
147
147
  if (!resolved.ok)
148
148
  return { operation: args.operation, ok: false, error: resolved.reason };
149
149
  const projectRoot = resolved.root;
@@ -246,8 +246,8 @@ export function registerStatusTool(ctx) {
246
246
  return [{ type: 'text', text: lines.filter(Boolean).join('\n') }];
247
247
  },
248
248
  },
249
- async execute(args) {
250
- const resolved = resolveProjectRoot(args.path);
249
+ async execute(args, exec) {
250
+ const resolved = resolveProjectRootForExec(exec, args.path);
251
251
  if (!resolved.ok)
252
252
  return { ok: false, error: resolved.reason };
253
253
  const projectRoot = resolved.root;
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { defineTool } from '@deepseek-ai/dsh-tools';
3
- import { loadEffectiveConfig, validateConfig, resolveProjectRoot } from "../config-loader.js";
3
+ import { loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from "../config-loader.js";
4
4
  import { applyConfigUpdates, readRawConfig, validateConfigUpdates, writeConfigFile, } from "../config-write.js";
5
5
  /**
6
6
  * Register the `iterate_config` tool.
@@ -62,8 +62,8 @@ export function registerConfigTool(ctx) {
62
62
  { type: 'text', text: JSON.stringify(value, null, 2) },
63
63
  ],
64
64
  },
65
- async execute(args) {
66
- const resolved = resolveProjectRoot(args.path);
65
+ async execute(args, exec) {
66
+ const resolved = resolveProjectRootForExec(exec, args.path);
67
67
  if (!resolved.ok) {
68
68
  return { found: false, error: resolved.reason };
69
69
  }
@@ -2,7 +2,7 @@ import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join, dirname, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools';
5
- import { resolveProjectRoot } from "../config-loader.js";
5
+ import { resolveProjectRootForExec } from "../config-loader.js";
6
6
  /** How many ancestor directories we walk up looking for a SKILL.md. */
7
7
  const MAX_SKILL_DIR_LOOKUP_DEPTH = 12;
8
8
  /** Maximum number of image attachments relayed into the context in one call. */
@@ -235,8 +235,8 @@ export function registerContextTool(ctx) {
235
235
  return [{ type: 'text', text: parts.join('\n\n') }];
236
236
  },
237
237
  },
238
- async execute(args) {
239
- const resolved = resolveProjectRoot(args.path);
238
+ async execute(args, exec) {
239
+ const resolved = resolveProjectRootForExec(exec, args.path);
240
240
  if (!resolved.ok) {
241
241
  return { found: false, error: resolved.reason, searched: [] };
242
242
  }
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools';
4
- import { resolveProjectRoot } from "../config-loader.js";
4
+ import { resolveProjectRootForExec } from "../config-loader.js";
5
5
  const LOG_DIR = '.iterate';
6
6
  const LOG_FILE = 'decision-log.jsonl';
7
7
  /** All valid DecisionLogEntry `type` values (must stay in sync with Types). */
@@ -147,8 +147,8 @@ export function registerDecisionLogTool(ctx) {
147
147
  { type: 'text', text: JSON.stringify(value, null, 2) },
148
148
  ],
149
149
  },
150
- async execute(args) {
151
- const resolved = resolveProjectRoot(args.path);
150
+ async execute(args, exec) {
151
+ const resolved = resolveProjectRootForExec(exec, args.path);
152
152
  if (!resolved.ok) {
153
153
  return { operation: args.operation, error: resolved.reason };
154
154
  }
package/dist/tools/fix.js CHANGED
@@ -19,7 +19,7 @@
19
19
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { defineTool } from '@deepseek-ai/dsh-tools';
22
- import { loadEffectiveConfig, resolveProjectRoot } from "../config-loader.js";
22
+ import { loadEffectiveConfig, resolveProjectRootForExec } from "../config-loader.js";
23
23
  import { countTouchedMethods } from "../method-scope.js";
24
24
  import { fixBackupPath, fixRegistryPath, fixesDir } from "../paths.js";
25
25
  import { appendDecisionEntry } from "./decision-log.js";
@@ -272,8 +272,8 @@ export function registerFixTool(ctx) {
272
272
  { type: 'text', text: value.ok ? `${value.diffSummary ?? 'fixed'} @ ${value.file} (id: ${value.id})` : `fix failed: ${value.error}` },
273
273
  ],
274
274
  },
275
- async execute(args) {
276
- const resolved = resolveProjectRoot(args.path);
275
+ async execute(args, exec) {
276
+ const resolved = resolveProjectRootForExec(exec, args.path);
277
277
  if (!resolved.ok)
278
278
  return { ok: false, error: resolved.reason };
279
279
  const projectRoot = resolved.root;
@@ -433,8 +433,8 @@ export function registerDiffTool(ctx) {
433
433
  return [{ type: 'text', text }];
434
434
  },
435
435
  },
436
- async execute(args) {
437
- const resolved = resolveProjectRoot(args.path);
436
+ async execute(args, exec) {
437
+ const resolved = resolveProjectRootForExec(exec, args.path);
438
438
  if (!resolved.ok)
439
439
  return { ok: false, error: resolved.reason };
440
440
  const projectRoot = resolved.root;
@@ -520,8 +520,8 @@ export function registerRollbackTool(ctx) {
520
520
  { type: 'text', text: value.ok ? `reverted fix ${value.id} in ${value.file}` : `rollback failed: ${value.error}` },
521
521
  ],
522
522
  },
523
- async execute(args) {
524
- const resolved = resolveProjectRoot(args.path);
523
+ async execute(args, exec) {
524
+ const resolved = resolveProjectRootForExec(exec, args.path);
525
525
  if (!resolved.ok)
526
526
  return { ok: false, error: resolved.reason };
527
527
  const projectRoot = resolved.root;
@@ -8,7 +8,7 @@
8
8
  * Complements `iterate_status` (compact summary) with the actual detail.
9
9
  */
10
10
  import { defineTool } from '@deepseek-ai/dsh-tools';
11
- import { resolveProjectRoot } from "../config-loader.js";
11
+ import { resolveProjectRootForExec } from "../config-loader.js";
12
12
  import { readDecisionEntries } from "./decision-log.js";
13
13
  import { readRegistry } from "./fix.js";
14
14
  const DEFAULT_LIMIT = 50;
@@ -117,8 +117,8 @@ export function registerHistoryTool(ctx) {
117
117
  return [{ type: 'text', text: lines.join('\n') }];
118
118
  },
119
119
  },
120
- async execute(args) {
121
- const resolved = resolveProjectRoot(args.path);
120
+ async execute(args, exec) {
121
+ const resolved = resolveProjectRootForExec(exec, args.path);
122
122
  if (!resolved.ok)
123
123
  return { ok: false, kind: 'history', error: resolved.reason };
124
124
  const projectRoot = resolved.root;
@@ -20,7 +20,7 @@
20
20
  import { existsSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
21
21
  import { join } from 'node:path';
22
22
  import { defineTool } from '@deepseek-ai/dsh-tools';
23
- import { resolveProjectRoot } from "../config-loader.js";
23
+ import { resolveProjectRootForExec } from "../config-loader.js";
24
24
  import { readDecisionEntries, appendDecisionEntry } from "./decision-log.js";
25
25
  import { readRegistry, removeRecord, recomputeRoundCounts } from "./fix.js";
26
26
  import { iterateDir, fixesDir, checkpointPath, fixRegistryPath } from "../paths.js";
@@ -225,8 +225,8 @@ export function registerPruneTool(ctx) {
225
225
  return [{ type: 'text', text: lines.join('\n') }];
226
226
  },
227
227
  },
228
- async execute(args) {
229
- const resolved = resolveProjectRoot(args.path);
228
+ async execute(args, exec) {
229
+ const resolved = resolveProjectRootForExec(exec, args.path);
230
230
  if (!resolved.ok)
231
231
  return { ok: false, dryRun: true, error: resolved.reason };
232
232
  const projectRoot = resolved.root;
@@ -1,5 +1,5 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
- import { loadEffectiveConfig, resolveProjectRoot } from "../config-loader.js";
2
+ import { loadEffectiveConfig, resolveProjectRootForExec } from "../config-loader.js";
3
3
  import { buildReviewPlan, buildReviewReport, sanitizeRounds, validateRoundsSchema, } from "../review.js";
4
4
  import { buildFinalReviewReport, metaReviewReport } from "../meta-review.js";
5
5
  import { evidenceToPlain, verifyFindings } from "../evidence.js";
@@ -106,8 +106,8 @@ export function registerReviewTool(ctx) {
106
106
  { type: 'text', text: JSON.stringify(value, null, 2) },
107
107
  ],
108
108
  },
109
- async execute(args) {
110
- const resolved = resolveProjectRoot(args.path);
109
+ async execute(args, exec) {
110
+ const resolved = resolveProjectRootForExec(exec, args.path);
111
111
  if (!resolved.ok) {
112
112
  return { operation: args.operation, error: resolved.reason };
113
113
  }
@@ -2,7 +2,7 @@ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools';
4
4
  import yaml from 'js-yaml';
5
- import { resolveProjectRoot } from "../config-loader.js";
5
+ import { resolveProjectRootForExec } from "../config-loader.js";
6
6
  const CONFIG_FILE = 'iterate.config.yaml';
7
7
  /** Personalization key that holds the known-intentional list. */
8
8
  const PERSONALIZATION_KEY = 'personalization';
@@ -272,8 +272,8 @@ export function registerTriageTool(ctx) {
272
272
  { type: 'text', text: JSON.stringify(value, null, 2) },
273
273
  ],
274
274
  },
275
- async execute(args) {
276
- const resolved = resolveProjectRoot(args.path);
275
+ async execute(args, exec) {
276
+ const resolved = resolveProjectRootForExec(exec, args.path);
277
277
  if (!resolved.ok) {
278
278
  return { operation: args.operation, error: resolved.reason };
279
279
  }
@@ -1,6 +1,6 @@
1
1
  import { exec } from 'node:child_process';
2
2
  import { defineTool } from '@deepseek-ai/dsh-tools';
3
- import { loadEffectiveConfig, isCommandAllowed, flattenCommands, resolveProjectRoot, } from "../config-loader.js";
3
+ import { loadEffectiveConfig, isCommandAllowed, flattenCommands, resolveProjectRootForExec, } from "../config-loader.js";
4
4
  const DEFAULT_TIMEOUT_MS = 120_000;
5
5
  /** Hard ceiling on a single validation command's runtime, so a model cannot
6
6
  * pin the tool open indefinitely via an unbounded `timeout` argument. */
@@ -103,8 +103,8 @@ export function registerValidateTool(ctx) {
103
103
  },
104
104
  ],
105
105
  },
106
- async execute(args) {
107
- const resolved = resolveProjectRoot(args.path);
106
+ async execute(args, exec) {
107
+ const resolved = resolveProjectRootForExec(exec, args.path);
108
108
  if (!resolved.ok) {
109
109
  return {
110
110
  allowed: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
4
4
  "description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus a dry-run pure-review mode with multi-round convergence and a meta-review that audits the report and emits a final review report.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -184,26 +184,50 @@ export type ProjectRootResult = { ok: true; root: string } | { ok: false; reason
184
184
  * path is unsafe; callers must short-circuit on the failure and return a
185
185
  * structured error instead of proceeding.
186
186
  */
187
- export function resolveProjectRoot(input?: string): ProjectRootResult {
187
+ /**
188
+ * Resolve a caller-supplied project root to a safe absolute path.
189
+ *
190
+ * Resolution order for the default (no explicit `path`) case:
191
+ * 1. `sessionCwd` — the absolute working directory the calling DSH session
192
+ * was created in (`exec.agent.session.header.cwd`). This is the
193
+ * authoritative workspace for the current conversation and is immune to
194
+ * where the web-service process happened to start.
195
+ * 2. the process cwd, when it is a usable directory (not `/` or the home
196
+ * dir — launchd/daemon-managed servers start with cwd=`/`);
197
+ * 3. the session workspace decoded from `DSH_SESSION_JSONL` (present when
198
+ * the DSH runtime injects per-session env into tool sub-processes).
199
+ */
200
+ export function resolveProjectRoot(input?: string, sessionCwd?: string): ProjectRootResult {
188
201
  const raw = (input ?? '').trim()
189
- const root = raw ? resolve(raw) : resolve(effectiveCwd())
202
+ const root = raw ? resolve(raw) : resolve(effectiveCwd(sessionCwd))
190
203
  if (!root || root === sep) {
191
204
  return { ok: false, reason: 'Refusing filesystem root as project root.' }
192
205
  }
193
206
  return { ok: true, root }
194
207
  }
195
208
 
209
+ /**
210
+ * Thin adapter for tool `execute(args, exec)` bodies: pull the session cwd
211
+ * from the DSH run context and hand it to {@link resolveProjectRoot}.
212
+ */
213
+ export function resolveProjectRootForExec(
214
+ exec: { agent?: { session?: { header?: { cwd?: string } } } } | undefined,
215
+ input?: string,
216
+ ): ProjectRootResult {
217
+ return resolveProjectRoot(input, exec?.agent?.session?.header?.cwd)
218
+ }
219
+
196
220
  /**
197
221
  * Resolve the default working directory for tools invoked without an explicit
198
- * `path`. Prefers the process cwd, but a daemon-managed web server can start
199
- * with cwd = `/` (e.g. launchd), which is not a usable project root. In that
200
- * case fall back to the session workspace encoded in `DSH_SESSION_JSONL`
222
+ * `path`. Prefers the caller-provided session cwd, then the process cwd, then
223
+ * the session workspace encoded in `DSH_SESSION_JSONL`
201
224
  * (`…/sessions/<encoded-workspace>/<session-id>/session.jsonl.zstd`), where
202
225
  * the workspace directory is `--`-wrapped with `/` → `-` and percent-encoded
203
226
  * bytes spelled as `~<hex>` (e.g. `/Volumes/Eng-Dev/iterate-skill` →
204
227
  * `--Volumes-Eng-Dev-iterate-skill--`).
205
228
  */
206
- function effectiveCwd(): string {
229
+ function effectiveCwd(sessionCwd?: string): string {
230
+ if (sessionCwd && sessionCwd !== sep && sessionCwd !== homedir()) return sessionCwd
207
231
  let cwd = ''
208
232
  try {
209
233
  cwd = process.cwd()
@@ -12,7 +12,7 @@
12
12
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
13
13
  import { defineTool } from '@deepseek-ai/dsh-tools'
14
14
  import type { JsonValue } from '@deepseek-ai/dsh-session'
15
- import { resolveProjectRoot } from '../config-loader.ts'
15
+ import { resolveProjectRootForExec } from '../config-loader.ts'
16
16
  import { checkpointPath, iterateDir } from '../paths.ts'
17
17
  import { readRegistry } from './fix.ts'
18
18
  import { readDecisionEntries } from './decision-log.ts'
@@ -166,8 +166,8 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
166
166
  ],
167
167
  },
168
168
 
169
- async execute(args) {
170
- const resolved = resolveProjectRoot(args.path)
169
+ async execute(args, exec) {
170
+ const resolved = resolveProjectRootForExec(exec, args.path)
171
171
  if (!resolved.ok) return { operation: args.operation, ok: false, error: resolved.reason }
172
172
  const projectRoot = resolved.root
173
173
 
@@ -274,8 +274,8 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
274
274
  },
275
275
  },
276
276
 
277
- async execute(args) {
278
- const resolved = resolveProjectRoot(args.path)
277
+ async execute(args, exec) {
278
+ const resolved = resolveProjectRootForExec(exec, args.path)
279
279
  if (!resolved.ok) return { ok: false, error: resolved.reason }
280
280
  const projectRoot = resolved.root
281
281
  const status = computeStatus({
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { defineTool } from '@deepseek-ai/dsh-tools'
3
3
  import type { JsonValue } from '@deepseek-ai/dsh-session'
4
- import { loadEffectiveConfig, validateConfig, resolveProjectRoot } from '../config-loader.ts'
4
+ import { loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from '../config-loader.ts'
5
5
  import {
6
6
  applyConfigUpdates,
7
7
  readRawConfig,
@@ -76,8 +76,8 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
76
76
  ],
77
77
  },
78
78
 
79
- async execute(args) {
80
- const resolved = resolveProjectRoot(args.path)
79
+ async execute(args, exec) {
80
+ const resolved = resolveProjectRootForExec(exec, args.path)
81
81
  if (!resolved.ok) {
82
82
  return { found: false, error: resolved.reason }
83
83
  }
@@ -2,7 +2,7 @@ import { readFileSync, existsSync } from 'node:fs'
2
2
  import { join, dirname, resolve } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
- import { resolveProjectRoot } from '../config-loader.ts'
5
+ import { resolveProjectRootForExec } from '../config-loader.ts'
6
6
 
7
7
  /** How many ancestor directories we walk up looking for a SKILL.md. */
8
8
  const MAX_SKILL_DIR_LOOKUP_DEPTH = 12
@@ -257,8 +257,8 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
257
257
  },
258
258
  },
259
259
 
260
- async execute(args) {
261
- const resolved = resolveProjectRoot(args.path)
260
+ async execute(args, exec) {
261
+ const resolved = resolveProjectRootForExec(exec, args.path)
262
262
  if (!resolved.ok) {
263
263
  return { found: false, error: resolved.reason, searched: [] }
264
264
  }
@@ -2,7 +2,7 @@ import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools'
4
4
  import type { JsonValue } from '@deepseek-ai/dsh-session'
5
- import { resolveProjectRoot } from '../config-loader.ts'
5
+ import { resolveProjectRootForExec } from '../config-loader.ts'
6
6
  import type { DecisionLogEntry } from '../types.ts'
7
7
 
8
8
  const LOG_DIR = '.iterate'
@@ -159,8 +159,8 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
159
159
  ],
160
160
  },
161
161
 
162
- async execute(args) {
163
- const resolved = resolveProjectRoot(args.path)
162
+ async execute(args, exec) {
163
+ const resolved = resolveProjectRootForExec(exec, args.path)
164
164
  if (!resolved.ok) {
165
165
  return { operation: args.operation, error: resolved.reason }
166
166
  }
package/src/tools/fix.ts CHANGED
@@ -21,7 +21,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
21
21
  import { join } from 'node:path'
22
22
  import { defineTool } from '@deepseek-ai/dsh-tools'
23
23
  import type { JsonValue } from '@deepseek-ai/dsh-session'
24
- import { loadEffectiveConfig, resolveProjectRoot } from '../config-loader.ts'
24
+ import { loadEffectiveConfig, resolveProjectRootForExec } from '../config-loader.ts'
25
25
  import { countTouchedMethods } from '../method-scope.ts'
26
26
  import { fixBackupPath, fixRegistryPath, fixesDir } from '../paths.ts'
27
27
  import { appendDecisionEntry } from './decision-log.ts'
@@ -284,8 +284,8 @@ export function registerFixTool(ctx: { tools: { register: (def: ReturnType<typeo
284
284
  ],
285
285
  },
286
286
 
287
- async execute(args) {
288
- const resolved = resolveProjectRoot(args.path)
287
+ async execute(args, exec) {
288
+ const resolved = resolveProjectRootForExec(exec, args.path)
289
289
  if (!resolved.ok) return { ok: false, error: resolved.reason }
290
290
  const projectRoot = resolved.root
291
291
  const { config } = loadEffectiveConfig(projectRoot)
@@ -454,8 +454,8 @@ export function registerDiffTool(ctx: { tools: { register: (def: ReturnType<type
454
454
  },
455
455
  },
456
456
 
457
- async execute(args) {
458
- const resolved = resolveProjectRoot(args.path)
457
+ async execute(args, exec) {
458
+ const resolved = resolveProjectRootForExec(exec, args.path)
459
459
  if (!resolved.ok) return { ok: false, error: resolved.reason }
460
460
  const projectRoot = resolved.root
461
461
  const registry = readRegistry(projectRoot)
@@ -544,8 +544,8 @@ export function registerRollbackTool(ctx: { tools: { register: (def: ReturnType<
544
544
  ],
545
545
  },
546
546
 
547
- async execute(args) {
548
- const resolved = resolveProjectRoot(args.path)
547
+ async execute(args, exec) {
548
+ const resolved = resolveProjectRootForExec(exec, args.path)
549
549
  if (!resolved.ok) return { ok: false, error: resolved.reason }
550
550
  const projectRoot = resolved.root
551
551
  const id = typeof args.id === 'string' ? args.id : ''
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { defineTool } from '@deepseek-ai/dsh-tools'
12
12
  import type { JsonValue } from '@deepseek-ai/dsh-session'
13
- import { resolveProjectRoot } from '../config-loader.ts'
13
+ import { resolveProjectRootForExec } from '../config-loader.ts'
14
14
  import { readDecisionEntries } from './decision-log.ts'
15
15
  import { readRegistry } from './fix.ts'
16
16
  import type { DecisionLogEntry, FixRegistry } from '../types.ts'
@@ -136,8 +136,8 @@ export function registerHistoryTool(ctx: { tools: { register: (def: ReturnType<t
136
136
  },
137
137
  },
138
138
 
139
- async execute(args) {
140
- const resolved = resolveProjectRoot(args.path)
139
+ async execute(args, exec) {
140
+ const resolved = resolveProjectRootForExec(exec, args.path)
141
141
  if (!resolved.ok) return { ok: false, kind: 'history', error: resolved.reason }
142
142
  const projectRoot = resolved.root
143
143
 
@@ -22,7 +22,7 @@ import { existsSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'node
22
22
  import { join } from 'node:path'
23
23
  import { defineTool } from '@deepseek-ai/dsh-tools'
24
24
  import type { JsonValue } from '@deepseek-ai/dsh-session'
25
- import { resolveProjectRoot } from '../config-loader.ts'
25
+ import { resolveProjectRootForExec } from '../config-loader.ts'
26
26
  import { readDecisionEntries, appendDecisionEntry } from './decision-log.ts'
27
27
  import { readRegistry, removeRecord, recomputeRoundCounts } from './fix.ts'
28
28
  import { iterateDir, fixesDir, checkpointPath, fixRegistryPath } from '../paths.ts'
@@ -265,8 +265,8 @@ export function registerPruneTool(ctx: { tools: { register: (def: ReturnType<typ
265
265
  },
266
266
  },
267
267
 
268
- async execute(args) {
269
- const resolved = resolveProjectRoot(args.path)
268
+ async execute(args, exec) {
269
+ const resolved = resolveProjectRootForExec(exec, args.path)
270
270
  if (!resolved.ok) return { ok: false, dryRun: true, error: resolved.reason }
271
271
  const projectRoot = resolved.root
272
272
  const retainDays = clampRetainDays(args.retainDays as number | undefined)
@@ -1,6 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from '@deepseek-ai/dsh-session'
3
- import { loadEffectiveConfig, resolveProjectRoot } from '../config-loader.ts'
3
+ import { loadEffectiveConfig, resolveProjectRootForExec } from '../config-loader.ts'
4
4
  import {
5
5
  buildReviewPlan,
6
6
  buildReviewReport,
@@ -131,8 +131,8 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
131
131
  ],
132
132
  },
133
133
 
134
- async execute(args) {
135
- const resolved = resolveProjectRoot(args.path)
134
+ async execute(args, exec) {
135
+ const resolved = resolveProjectRootForExec(exec, args.path)
136
136
  if (!resolved.ok) {
137
137
  return { operation: args.operation, error: resolved.reason }
138
138
  }
@@ -3,7 +3,7 @@ import { join } from 'node:path'
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools'
4
4
  import type { JsonValue } from '@deepseek-ai/dsh-session'
5
5
  import yaml from 'js-yaml'
6
- import { resolveProjectRoot } from '../config-loader.ts'
6
+ import { resolveProjectRootForExec } from '../config-loader.ts'
7
7
  import type { KnownIntentional } from '../types.ts'
8
8
 
9
9
  const CONFIG_FILE = 'iterate.config.yaml'
@@ -313,8 +313,8 @@ export function registerTriageTool(ctx: { tools: { register: (def: ReturnType<ty
313
313
  ],
314
314
  },
315
315
 
316
- async execute(args) {
317
- const resolved = resolveProjectRoot(args.path)
316
+ async execute(args, exec) {
317
+ const resolved = resolveProjectRootForExec(exec, args.path)
318
318
  if (!resolved.ok) {
319
319
  return { operation: args.operation, error: resolved.reason }
320
320
  }
@@ -4,7 +4,7 @@ import {
4
4
  loadEffectiveConfig,
5
5
  isCommandAllowed,
6
6
  flattenCommands,
7
- resolveProjectRoot,
7
+ resolveProjectRootForExec,
8
8
  } from '../config-loader.ts'
9
9
  import type { ValidationResult } from '../types.ts'
10
10
 
@@ -126,8 +126,8 @@ export function registerValidateTool(ctx: { tools: { register: (def: ReturnType<
126
126
  ],
127
127
  },
128
128
 
129
- async execute(args) {
130
- const resolved = resolveProjectRoot(args.path)
129
+ async execute(args, exec) {
130
+ const resolved = resolveProjectRootForExec(exec, args.path)
131
131
  if (!resolved.ok) {
132
132
  return {
133
133
  allowed: false,