iterate-plugin 2.9.2 → 2.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/lib/client.js CHANGED
@@ -43,6 +43,20 @@ var SEVERITY_COLOR = {
43
43
  medium: "#eab308",
44
44
  low: "#6b7280"
45
45
  };
46
+ function safeGet(o, key) {
47
+ try {
48
+ return o[key];
49
+ } catch {
50
+ return void 0;
51
+ }
52
+ }
53
+ function safeKeys(o) {
54
+ try {
55
+ return Object.keys(o);
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
46
60
  function scanSessionForResume(obj, seen, maxDepth = 20) {
47
61
  if (maxDepth <= 0) return 0;
48
62
  if (!obj || typeof obj !== "object") return 0;
@@ -54,27 +68,28 @@ function scanSessionForResume(obj, seen, maxDepth = 20) {
54
68
  /** @type {Record<string, unknown>} */
55
69
  obj
56
70
  );
57
- if (direct.type === "resume") {
71
+ if (safeGet(direct, "type") === "resume") {
58
72
  const data = (
59
73
  /** @type {Record<string, unknown>} */
60
- direct.data || {}
74
+ safeGet(direct, "data") || {}
61
75
  );
62
- if (typeof data.resumeCount === "number" && data.resumeCount > best) {
63
- best = data.resumeCount;
76
+ if (typeof safeGet(data, "resumeCount") === "number" && safeGet(data, "resumeCount") > best) {
77
+ best = safeGet(data, "resumeCount");
64
78
  }
65
79
  }
66
- if (direct.entry && typeof direct.entry === "object") {
80
+ const directEntry = safeGet(direct, "entry");
81
+ if (directEntry && typeof directEntry === "object") {
67
82
  const entry = (
68
83
  /** @type {Record<string, unknown>} */
69
- direct.entry
84
+ directEntry
70
85
  );
71
- if (entry.type === "resume") {
86
+ if (safeGet(entry, "type") === "resume") {
72
87
  const data = (
73
88
  /** @type {Record<string, unknown>} */
74
- entry.data || {}
89
+ safeGet(entry, "data") || {}
75
90
  );
76
- if (typeof data.resumeCount === "number" && data.resumeCount > best) {
77
- best = data.resumeCount;
91
+ if (typeof safeGet(data, "resumeCount") === "number" && safeGet(data, "resumeCount") > best) {
92
+ best = safeGet(data, "resumeCount");
78
93
  }
79
94
  }
80
95
  }
@@ -85,8 +100,8 @@ function scanSessionForResume(obj, seen, maxDepth = 20) {
85
100
  }
86
101
  return best;
87
102
  }
88
- for (const key of Object.keys(direct)) {
89
- const val = direct[key];
103
+ for (const key of safeKeys(direct)) {
104
+ const val = safeGet(direct, key);
90
105
  if (val && typeof val === "object") {
91
106
  const found = scanSessionForResume(val, s, maxDepth - 1);
92
107
  if (found > best) best = found;
@@ -107,15 +122,15 @@ function countSessionImages(session) {
107
122
  obj
108
123
  );
109
124
  let ref = null;
110
- if (o.type === "image" && o.attachment && typeof o.attachment === "object") {
125
+ if (safeGet(o, "type") === "image" && safeGet(o, "attachment") && typeof safeGet(o, "attachment") === "object") {
111
126
  ref = /** @type {Record<string, unknown>} */
112
- o.attachment;
127
+ safeGet(o, "attachment");
113
128
  }
114
- if (!ref && typeof o.mediaType === "string" && String(o.mediaType).startsWith("image/")) {
129
+ if (!ref && typeof safeGet(o, "mediaType") === "string" && String(safeGet(o, "mediaType")).startsWith("image/")) {
115
130
  ref = o;
116
131
  }
117
132
  if (ref) {
118
- const id = typeof ref.attachmentId === "string" ? ref.attachmentId : null;
133
+ const id = typeof safeGet(ref, "attachmentId") === "string" ? safeGet(ref, "attachmentId") : null;
119
134
  if (id) {
120
135
  if (!ids.has(id)) {
121
136
  ids.add(id);
@@ -129,8 +144,8 @@ function countSessionImages(session) {
129
144
  for (const item of obj) walk(item, depth - 1);
130
145
  return;
131
146
  }
132
- for (const key of Object.keys(o)) {
133
- const val = o[key];
147
+ for (const key of safeKeys(o)) {
148
+ const val = safeGet(o, key);
134
149
  if (val && typeof val === "object") walk(val, depth - 1);
135
150
  }
136
151
  };
@@ -144,7 +159,8 @@ function isReviewReport(obj) {
144
159
  /** @type {Record<string, unknown>} */
145
160
  obj
146
161
  );
147
- return typeof o.convergence === "object" && o.convergence !== null && Array.isArray(o.findings) && Array.isArray(o.rounds);
162
+ const convergence = safeGet(o, "convergence");
163
+ return typeof convergence === "object" && convergence !== null && Array.isArray(safeGet(o, "findings")) && Array.isArray(safeGet(o, "rounds"));
148
164
  }
149
165
  function findReportInObject(obj, seen, maxDepth = 20) {
150
166
  if (maxDepth <= 0) return null;
@@ -167,8 +183,8 @@ function findReportInObject(obj, seen, maxDepth = 20) {
167
183
  /** @type {Record<string, unknown>} */
168
184
  obj
169
185
  );
170
- for (const key of Object.keys(o)) {
171
- const val = o[key];
186
+ for (const key of safeKeys(o)) {
187
+ const val = safeGet(o, key);
172
188
  if (val && typeof val === "object") {
173
189
  const found = findReportInObject(val, s, maxDepth - 1);
174
190
  if (found) return found;
@@ -184,54 +200,58 @@ function scanSessionForReport(session) {
184
200
  /** @type {Record<string, unknown>} */
185
201
  session
186
202
  );
187
- if (Array.isArray(s.toolCalls)) {
203
+ const toolCalls = safeGet(s, "toolCalls");
204
+ if (Array.isArray(toolCalls)) {
188
205
  const calls = (
189
206
  /** @type {Array<Record<string, unknown>>} */
190
- s.toolCalls
207
+ toolCalls
191
208
  );
192
209
  for (let i = calls.length - 1; i >= 0; i--) {
193
210
  const call = calls[i];
194
211
  if (!call) continue;
195
- if (call.tool === "iterate_review" || String(call.tool ?? "").endsWith("iterate_review")) {
196
- const result = call.result;
212
+ if (safeGet(call, "tool") === "iterate_review" || String(safeGet(call, "tool") ?? "").endsWith("iterate_review")) {
213
+ const result = safeGet(call, "result");
197
214
  if (result && typeof result === "object") {
198
215
  const r = (
199
216
  /** @type {Record<string, unknown>} */
200
217
  result
201
218
  );
202
- if (r.report && typeof r.report === "object") {
219
+ const report = safeGet(r, "report");
220
+ if (report && typeof report === "object") {
203
221
  return (
204
222
  /** @type {Record<string, unknown>} */
205
- r.report
223
+ report
206
224
  );
207
225
  }
208
226
  }
209
227
  }
210
228
  }
211
229
  }
212
- if (Array.isArray(s.messages)) {
230
+ const messages = safeGet(s, "messages");
231
+ if (Array.isArray(messages)) {
213
232
  const msgs = (
214
233
  /** @type {Array<Record<string, unknown>>} */
215
- s.messages
234
+ messages
216
235
  );
217
236
  for (let i = msgs.length - 1; i >= 0; i--) {
218
237
  const msg = msgs[i];
219
- if (!msg || !Array.isArray(msg.tool_calls)) continue;
238
+ const msgCalls = msg && Array.isArray(safeGet(msg, "tool_calls")) ? safeGet(msg, "tool_calls") : null;
239
+ if (!msg || !msgCalls) continue;
220
240
  const calls = (
221
241
  /** @type {Array<Record<string, unknown>>} */
222
- msg.tool_calls
242
+ msgCalls
223
243
  );
224
244
  for (const call of calls) {
225
245
  if (!call) continue;
226
- const fn = call.function;
246
+ const fn = safeGet(call, "function");
227
247
  if (fn && typeof fn === "object") {
228
248
  const f = (
229
249
  /** @type {Record<string, unknown>} */
230
250
  fn
231
251
  );
232
- if (String(f.name ?? "").endsWith("iterate_review")) {
252
+ if (String(safeGet(f, "name") ?? "").endsWith("iterate_review")) {
233
253
  try {
234
- const args = JSON.parse(String(f.arguments ?? "{}"));
254
+ const args = JSON.parse(String(safeGet(f, "arguments") ?? "{}"));
235
255
  const found = findReportInObject(args);
236
256
  if (found) return found;
237
257
  } catch {
@@ -249,8 +269,8 @@ function isRunSummary(obj) {
249
269
  /** @type {Record<string, unknown>} */
250
270
  obj
251
271
  );
252
- const final = o.finalReport;
253
- return !!final && typeof final === "object" && (final.verdict === "approved" || final.verdict === "needs_revision");
272
+ const final = safeGet(o, "finalReport");
273
+ return !!final && typeof final === "object" && (safeGet(final, "verdict") === "approved" || safeGet(final, "verdict") === "needs_revision");
254
274
  }
255
275
  function findRunSummaryInObject(obj, seen, maxDepth = 20) {
256
276
  if (maxDepth <= 0) return null;
@@ -273,8 +293,8 @@ function findRunSummaryInObject(obj, seen, maxDepth = 20) {
273
293
  /** @type {Record<string, unknown>} */
274
294
  obj
275
295
  );
276
- for (const key of Object.keys(o)) {
277
- const val = o[key];
296
+ for (const key of safeKeys(o)) {
297
+ const val = safeGet(o, key);
278
298
  if (val && typeof val === "object") {
279
299
  const found = findRunSummaryInObject(val, s, maxDepth - 1);
280
300
  if (found) return found;
@@ -290,29 +310,31 @@ function scanSessionForRunSummary(session) {
290
310
  /** @type {Record<string, unknown>} */
291
311
  session
292
312
  );
293
- if (Array.isArray(s.toolCalls)) {
313
+ const toolCalls = safeGet(s, "toolCalls");
314
+ if (Array.isArray(toolCalls)) {
294
315
  const calls = (
295
316
  /** @type {Array<Record<string, unknown>>} */
296
- s.toolCalls
317
+ toolCalls
297
318
  );
298
319
  for (let i = calls.length - 1; i >= 0; i--) {
299
320
  const call = calls[i];
300
321
  if (!call) continue;
301
- if (call.tool === "workflow" || String(call.tool ?? "").endsWith("workflow")) {
302
- const found = findRunSummaryInObject(call.result, void 0, 24);
322
+ if (safeGet(call, "tool") === "workflow" || String(safeGet(call, "tool") ?? "").endsWith("workflow")) {
323
+ const found = findRunSummaryInObject(safeGet(call, "result"), void 0, 24);
303
324
  if (found) return found;
304
325
  }
305
326
  }
306
327
  }
307
- if (Array.isArray(s.messages)) {
328
+ const messages = safeGet(s, "messages");
329
+ if (Array.isArray(messages)) {
308
330
  const msgs = (
309
331
  /** @type {Array<Record<string, unknown>>} */
310
- s.messages
332
+ messages
311
333
  );
312
334
  for (let i = msgs.length - 1; i >= 0; i--) {
313
335
  const msg = msgs[i];
314
336
  if (!msg) continue;
315
- const found = findRunSummaryInObject(msg.content);
337
+ const found = findRunSummaryInObject(safeGet(msg, "content"));
316
338
  if (found) return found;
317
339
  }
318
340
  }
@@ -326,22 +348,22 @@ function extractVerdict(runSummary) {
326
348
  );
327
349
  const final = (
328
350
  /** @type {Record<string, unknown>} */
329
- o.finalReport
351
+ safeGet(o, "finalReport")
330
352
  );
331
- const meta = final.metaReview && typeof final.metaReview === "object" ? (
353
+ const meta = safeGet(final, "metaReview") && typeof safeGet(final, "metaReview") === "object" ? (
332
354
  /** @type {Record<string, unknown>} */
333
- final.metaReview
355
+ safeGet(final, "metaReview")
334
356
  ) : {};
335
- const issues = Array.isArray(meta.issues) ? meta.issues : [];
336
- const roundsVal = o.rounds;
357
+ const issues = Array.isArray(safeGet(meta, "issues")) ? safeGet(meta, "issues") : [];
358
+ const roundsVal = safeGet(o, "rounds");
337
359
  const totalRounds = typeof roundsVal === "number" ? roundsVal : Array.isArray(roundsVal) ? roundsVal.length : 0;
338
360
  return {
339
- verdict: final.verdict === "needs_revision" ? "needs_revision" : "approved",
361
+ verdict: safeGet(final, "verdict") === "needs_revision" ? "needs_revision" : "approved",
340
362
  reportIssues: issues.length,
341
- checksRun: typeof meta.checksRun === "number" ? meta.checksRun : 0,
342
- converged: o.converged === true,
363
+ checksRun: typeof safeGet(meta, "checksRun") === "number" ? safeGet(meta, "checksRun") : 0,
364
+ converged: safeGet(o, "converged") === true,
343
365
  totalRounds,
344
- totalFindings: typeof o.totalFindings === "number" ? o.totalFindings : 0
366
+ totalFindings: typeof safeGet(o, "totalFindings") === "number" ? safeGet(o, "totalFindings") : 0
345
367
  };
346
368
  }
347
369
  function normalizeReport(report) {
package/lib/parse.js CHANGED
@@ -28,6 +28,31 @@ export const SEVERITY_COLOR = {
28
28
  low: '#6b7280',
29
29
  }
30
30
 
31
+ // ─── Safe property access ────────────────────────────────────────────────────
32
+ // Session snapshots handed to the client UI can be cordis service proxies or
33
+ // contain proxy references (owner share objects). Reading an un-injected
34
+ // service name off such a proxy throws `cannot get property "x" without
35
+ // inject`. All deep scans below therefore read through these helpers so a
36
+ // hostile/proxied object degrades to "no match" instead of crashing the slot.
37
+
38
+ /** Read one property that may sit on a cordis service proxy; never throws. */
39
+ function safeGet(o, key) {
40
+ try {
41
+ return o[key]
42
+ } catch {
43
+ return undefined
44
+ }
45
+ }
46
+
47
+ /** Keys of an object that may be a cordis service proxy; never throws. */
48
+ function safeKeys(o) {
49
+ try {
50
+ return Object.keys(o)
51
+ } catch {
52
+ return []
53
+ }
54
+ }
55
+
31
56
  // ─── Interruption / resume + image attachment detection ──────────────────────
32
57
 
33
58
  /**
@@ -55,19 +80,20 @@ export function scanSessionForResume(obj, seen, maxDepth = 20) {
55
80
 
56
81
  // Direct marker: { type: "resume", data: { resumeCount } }.
57
82
  const direct = /** @type {Record<string, unknown>} */ (obj)
58
- if (direct.type === 'resume') {
59
- const data = /** @type {Record<string, unknown>} */ (direct.data || {})
60
- if (typeof data.resumeCount === 'number' && data.resumeCount > best) {
61
- best = data.resumeCount
83
+ if (safeGet(direct, 'type') === 'resume') {
84
+ const data = /** @type {Record<string, unknown>} */ (safeGet(direct, 'data') || {})
85
+ if (typeof safeGet(data, 'resumeCount') === 'number' && safeGet(data, 'resumeCount') > best) {
86
+ best = safeGet(data, 'resumeCount')
62
87
  }
63
88
  }
64
89
  // Nested entry: { entry: { type: "resume", data: { resumeCount } } }.
65
- if (direct.entry && typeof direct.entry === 'object') {
66
- const entry = /** @type {Record<string, unknown>} */ (direct.entry)
67
- if (entry.type === 'resume') {
68
- const data = /** @type {Record<string, unknown>} */ (entry.data || {})
69
- if (typeof data.resumeCount === 'number' && data.resumeCount > best) {
70
- best = data.resumeCount
90
+ const directEntry = safeGet(direct, 'entry')
91
+ if (directEntry && typeof directEntry === 'object') {
92
+ const entry = /** @type {Record<string, unknown>} */ (directEntry)
93
+ if (safeGet(entry, 'type') === 'resume') {
94
+ const data = /** @type {Record<string, unknown>} */ (safeGet(entry, 'data') || {})
95
+ if (typeof safeGet(data, 'resumeCount') === 'number' && safeGet(data, 'resumeCount') > best) {
96
+ best = safeGet(data, 'resumeCount')
71
97
  }
72
98
  }
73
99
  }
@@ -80,8 +106,8 @@ export function scanSessionForResume(obj, seen, maxDepth = 20) {
80
106
  return best
81
107
  }
82
108
 
83
- for (const key of Object.keys(direct)) {
84
- const val = direct[key]
109
+ for (const key of safeKeys(direct)) {
110
+ const val = safeGet(direct, key)
85
111
  if (val && typeof val === 'object') {
86
112
  const found = scanSessionForResume(val, s, maxDepth - 1)
87
113
  if (found > best) best = found
@@ -115,15 +141,15 @@ export function countSessionImages(session) {
115
141
 
116
142
  // Image block: { type: "image", attachment: { ...ref } }.
117
143
  let ref = null
118
- if (o.type === 'image' && o.attachment && typeof o.attachment === 'object') {
119
- ref = /** @type {Record<string, unknown>} */ (o.attachment)
144
+ if (safeGet(o, 'type') === 'image' && safeGet(o, 'attachment') && typeof safeGet(o, 'attachment') === 'object') {
145
+ ref = /** @type {Record<string, unknown>} */ (safeGet(o, 'attachment'))
120
146
  }
121
147
  // Raw attachment reference shape.
122
- if (!ref && typeof o.mediaType === 'string' && String(o.mediaType).startsWith('image/')) {
148
+ if (!ref && typeof safeGet(o, 'mediaType') === 'string' && String(safeGet(o, 'mediaType')).startsWith('image/')) {
123
149
  ref = o
124
150
  }
125
151
  if (ref) {
126
- const id = typeof ref.attachmentId === 'string' ? ref.attachmentId : null
152
+ const id = typeof safeGet(ref, 'attachmentId') === 'string' ? safeGet(ref, 'attachmentId') : null
127
153
  if (id) {
128
154
  if (!ids.has(id)) { ids.add(id); count += 1 }
129
155
  } else {
@@ -135,8 +161,8 @@ export function countSessionImages(session) {
135
161
  for (const item of obj) walk(item, depth - 1)
136
162
  return
137
163
  }
138
- for (const key of Object.keys(o)) {
139
- const val = o[key]
164
+ for (const key of safeKeys(o)) {
165
+ const val = safeGet(o, key)
140
166
  if (val && typeof val === 'object') walk(val, depth - 1)
141
167
  }
142
168
  }
@@ -159,11 +185,12 @@ export function countSessionImages(session) {
159
185
  export function isReviewReport(obj) {
160
186
  if (!obj || typeof obj !== 'object') return false
161
187
  const o = /** @type {Record<string, unknown>} */ (obj)
188
+ const convergence = safeGet(o, 'convergence')
162
189
  return (
163
- typeof o.convergence === 'object' &&
164
- o.convergence !== null &&
165
- Array.isArray(o.findings) &&
166
- Array.isArray(o.rounds)
190
+ typeof convergence === 'object' &&
191
+ convergence !== null &&
192
+ Array.isArray(safeGet(o, 'findings')) &&
193
+ Array.isArray(safeGet(o, 'rounds'))
167
194
  )
168
195
  }
169
196
 
@@ -201,8 +228,8 @@ export function findReportInObject(obj, seen, maxDepth = 20) {
201
228
 
202
229
  // Check object values
203
230
  const o = /** @type {Record<string, unknown>} */ (obj)
204
- for (const key of Object.keys(o)) {
205
- const val = o[key]
231
+ for (const key of safeKeys(o)) {
232
+ const val = safeGet(o, key)
206
233
  if (val && typeof val === 'object') {
207
234
  // Check leaf values that are arrays or objects
208
235
  const found = findReportInObject(val, s, maxDepth - 1)
@@ -231,17 +258,19 @@ export function scanSessionForReport(session) {
231
258
  const s = /** @type {Record<string, unknown>} */ (session)
232
259
 
233
260
  // Common pattern: session.toolCalls[].result.report
234
- if (Array.isArray(s.toolCalls)) {
235
- const calls = /** @type {Array<Record<string, unknown>>} */ (s.toolCalls)
261
+ const toolCalls = safeGet(s, 'toolCalls')
262
+ if (Array.isArray(toolCalls)) {
263
+ const calls = /** @type {Array<Record<string, unknown>>} */ (toolCalls)
236
264
  for (let i = calls.length - 1; i >= 0; i--) {
237
265
  const call = calls[i]
238
266
  if (!call) continue
239
- if (call.tool === 'iterate_review' || String(call.tool ?? '').endsWith('iterate_review')) {
240
- const result = call.result
267
+ if (safeGet(call, 'tool') === 'iterate_review' || String(safeGet(call, 'tool') ?? '').endsWith('iterate_review')) {
268
+ const result = safeGet(call, 'result')
241
269
  if (result && typeof result === 'object') {
242
270
  const r = /** @type {Record<string, unknown>} */ (result)
243
- if (r.report && typeof r.report === 'object') {
244
- return /** @type {Record<string, unknown>} */ (r.report)
271
+ const report = safeGet(r, 'report')
272
+ if (report && typeof report === 'object') {
273
+ return /** @type {Record<string, unknown>} */ (report)
245
274
  }
246
275
  }
247
276
  }
@@ -249,21 +278,23 @@ export function scanSessionForReport(session) {
249
278
  }
250
279
 
251
280
  // Common pattern: session.messages[].tool_calls[].function.arguments
252
- if (Array.isArray(s.messages)) {
253
- const msgs = /** @type {Array<Record<string, unknown>>} */ (s.messages)
281
+ const messages = safeGet(s, 'messages')
282
+ if (Array.isArray(messages)) {
283
+ const msgs = /** @type {Array<Record<string, unknown>>} */ (messages)
254
284
  for (let i = msgs.length - 1; i >= 0; i--) {
255
285
  const msg = msgs[i]
256
- if (!msg || !Array.isArray(msg.tool_calls)) continue
257
- const calls = /** @type {Array<Record<string, unknown>>} */ (msg.tool_calls)
286
+ const msgCalls = msg && Array.isArray(safeGet(msg, 'tool_calls')) ? safeGet(msg, 'tool_calls') : null
287
+ if (!msg || !msgCalls) continue
288
+ const calls = /** @type {Array<Record<string, unknown>>} */ (msgCalls)
258
289
  for (const call of calls) {
259
290
  if (!call) continue
260
- const fn = call.function
291
+ const fn = safeGet(call, 'function')
261
292
  if (fn && typeof fn === 'object') {
262
293
  const f = /** @type {Record<string, unknown>} */ (fn)
263
- if (String(f.name ?? '').endsWith('iterate_review')) {
294
+ if (String(safeGet(f, 'name') ?? '').endsWith('iterate_review')) {
264
295
  // Try to parse arguments
265
296
  try {
266
- const args = JSON.parse(String(f.arguments ?? '{}'))
297
+ const args = JSON.parse(String(safeGet(f, 'arguments') ?? '{}'))
267
298
  const found = findReportInObject(args)
268
299
  if (found) return found
269
300
  } catch {
@@ -296,10 +327,10 @@ export function scanSessionForReport(session) {
296
327
  export function isRunSummary(obj) {
297
328
  if (!obj || typeof obj !== 'object') return false
298
329
  const o = /** @type {Record<string, unknown>} */ (obj)
299
- const final = o.finalReport
330
+ const final = safeGet(o, 'finalReport')
300
331
  return !!final &&
301
332
  typeof final === 'object' &&
302
- (final.verdict === 'approved' || final.verdict === 'needs_revision')
333
+ (safeGet(final, 'verdict') === 'approved' || safeGet(final, 'verdict') === 'needs_revision')
303
334
  }
304
335
 
305
336
  /**
@@ -330,8 +361,8 @@ export function findRunSummaryInObject(obj, seen, maxDepth = 20) {
330
361
  }
331
362
 
332
363
  const o = /** @type {Record<string, unknown>} */ (obj)
333
- for (const key of Object.keys(o)) {
334
- const val = o[key]
364
+ for (const key of safeKeys(o)) {
365
+ const val = safeGet(o, key)
335
366
  if (val && typeof val === 'object') {
336
367
  const found = findRunSummaryInObject(val, s, maxDepth - 1)
337
368
  if (found) return found
@@ -357,25 +388,27 @@ export function scanSessionForRunSummary(session) {
357
388
  const s = /** @type {Record<string, unknown>} */ (session)
358
389
 
359
390
  // Common pattern: session.toolCalls[].result contains a run summary.
360
- if (Array.isArray(s.toolCalls)) {
361
- const calls = /** @type {Array<Record<string, unknown>>} */ (s.toolCalls)
391
+ const toolCalls = safeGet(s, 'toolCalls')
392
+ if (Array.isArray(toolCalls)) {
393
+ const calls = /** @type {Array<Record<string, unknown>>} */ (toolCalls)
362
394
  for (let i = calls.length - 1; i >= 0; i--) {
363
395
  const call = calls[i]
364
396
  if (!call) continue
365
- if (call.tool === 'workflow' || String(call.tool ?? '').endsWith('workflow')) {
366
- const found = findRunSummaryInObject(call.result, undefined, 24)
397
+ if (safeGet(call, 'tool') === 'workflow' || String(safeGet(call, 'tool') ?? '').endsWith('workflow')) {
398
+ const found = findRunSummaryInObject(safeGet(call, 'result'), undefined, 24)
367
399
  if (found) return found
368
400
  }
369
401
  }
370
402
  }
371
403
 
372
404
  // Common pattern: assistant message content holding the workflow return.
373
- if (Array.isArray(s.messages)) {
374
- const msgs = /** @type {Array<Record<string, unknown>>} */ (s.messages)
405
+ const messages = safeGet(s, 'messages')
406
+ if (Array.isArray(messages)) {
407
+ const msgs = /** @type {Array<Record<string, unknown>>} */ (messages)
375
408
  for (let i = msgs.length - 1; i >= 0; i--) {
376
409
  const msg = msgs[i]
377
410
  if (!msg) continue
378
- const found = findRunSummaryInObject(msg.content)
411
+ const found = findRunSummaryInObject(safeGet(msg, 'content'))
379
412
  if (found) return found
380
413
  }
381
414
  }
@@ -393,21 +426,21 @@ export function scanSessionForRunSummary(session) {
393
426
  export function extractVerdict(runSummary) {
394
427
  if (!isRunSummary(runSummary)) return null
395
428
  const o = /** @type {Record<string, unknown>} */ (runSummary)
396
- const final = /** @type {Record<string, unknown>} */ (o.finalReport)
397
- const meta = final.metaReview && typeof final.metaReview === 'object'
398
- ? /** @type {Record<string, unknown>} */ (final.metaReview)
429
+ const final = /** @type {Record<string, unknown>} */ (safeGet(o, 'finalReport'))
430
+ const meta = safeGet(final, 'metaReview') && typeof safeGet(final, 'metaReview') === 'object'
431
+ ? /** @type {Record<string, unknown>} */ (safeGet(final, 'metaReview'))
399
432
  : {}
400
- const issues = Array.isArray(meta.issues) ? meta.issues : []
433
+ const issues = Array.isArray(safeGet(meta, 'issues')) ? safeGet(meta, 'issues') : []
401
434
  // `totalRounds` may be a bare number (dry-run returns `rounds`) or a count.
402
- const roundsVal = o.rounds
435
+ const roundsVal = safeGet(o, 'rounds')
403
436
  const totalRounds = typeof roundsVal === 'number' ? roundsVal : (Array.isArray(roundsVal) ? roundsVal.length : 0)
404
437
  return {
405
- verdict: final.verdict === 'needs_revision' ? 'needs_revision' : 'approved',
438
+ verdict: safeGet(final, 'verdict') === 'needs_revision' ? 'needs_revision' : 'approved',
406
439
  reportIssues: issues.length,
407
- checksRun: typeof meta.checksRun === 'number' ? meta.checksRun : 0,
408
- converged: o.converged === true,
440
+ checksRun: typeof safeGet(meta, 'checksRun') === 'number' ? safeGet(meta, 'checksRun') : 0,
441
+ converged: safeGet(o, 'converged') === true,
409
442
  totalRounds,
410
- totalFindings: typeof o.totalFindings === 'number' ? o.totalFindings : 0,
443
+ totalFindings: typeof safeGet(o, 'totalFindings') === 'number' ? safeGet(o, 'totalFindings') : 0,
411
444
  }
412
445
  }
413
446
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-plugin",
3
- "version": "2.9.2",
3
+ "version": "2.9.4",
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,