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.
- package/dist/config-loader.js +27 -6
- package/dist/tools/checkpoint.js +5 -5
- package/dist/tools/config.js +3 -3
- package/dist/tools/context.js +3 -3
- package/dist/tools/decision-log.js +3 -3
- package/dist/tools/fix.js +7 -7
- package/dist/tools/history.js +3 -3
- package/dist/tools/prune.js +3 -3
- package/dist/tools/review.js +3 -3
- package/dist/tools/triage.js +3 -3
- package/dist/tools/validate.js +3 -3
- package/lib/client.js +76 -54
- package/lib/parse.js +90 -57
- package/package.json +1 -1
- package/src/config-loader.ts +30 -6
- package/src/tools/checkpoint.ts +5 -5
- package/src/tools/config.ts +3 -3
- package/src/tools/context.ts +3 -3
- package/src/tools/decision-log.ts +3 -3
- package/src/tools/fix.ts +7 -7
- package/src/tools/history.ts +3 -3
- package/src/tools/prune.ts +3 -3
- package/src/tools/review.ts +3 -3
- package/src/tools/triage.ts +3 -3
- package/src/tools/validate.ts +3 -3
package/dist/config-loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
181
|
-
*
|
|
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();
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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;
|
package/dist/tools/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
|
-
import { loadEffectiveConfig, validateConfig,
|
|
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 =
|
|
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
|
}
|
package/dist/tools/context.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|
package/dist/tools/history.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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;
|
package/dist/tools/prune.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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;
|
package/dist/tools/review.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
-
import { loadEffectiveConfig,
|
|
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 =
|
|
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
|
}
|
package/dist/tools/triage.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
}
|
package/dist/tools/validate.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
|
71
|
+
if (safeGet(direct, "type") === "resume") {
|
|
58
72
|
const data = (
|
|
59
73
|
/** @type {Record<string, unknown>} */
|
|
60
|
-
direct
|
|
74
|
+
safeGet(direct, "data") || {}
|
|
61
75
|
);
|
|
62
|
-
if (typeof data
|
|
63
|
-
best = data
|
|
76
|
+
if (typeof safeGet(data, "resumeCount") === "number" && safeGet(data, "resumeCount") > best) {
|
|
77
|
+
best = safeGet(data, "resumeCount");
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
|
-
|
|
80
|
+
const directEntry = safeGet(direct, "entry");
|
|
81
|
+
if (directEntry && typeof directEntry === "object") {
|
|
67
82
|
const entry = (
|
|
68
83
|
/** @type {Record<string, unknown>} */
|
|
69
|
-
|
|
84
|
+
directEntry
|
|
70
85
|
);
|
|
71
|
-
if (entry
|
|
86
|
+
if (safeGet(entry, "type") === "resume") {
|
|
72
87
|
const data = (
|
|
73
88
|
/** @type {Record<string, unknown>} */
|
|
74
|
-
entry
|
|
89
|
+
safeGet(entry, "data") || {}
|
|
75
90
|
);
|
|
76
|
-
if (typeof data
|
|
77
|
-
best = data
|
|
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
|
|
89
|
-
const val = direct
|
|
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
|
|
125
|
+
if (safeGet(o, "type") === "image" && safeGet(o, "attachment") && typeof safeGet(o, "attachment") === "object") {
|
|
111
126
|
ref = /** @type {Record<string, unknown>} */
|
|
112
|
-
o
|
|
127
|
+
safeGet(o, "attachment");
|
|
113
128
|
}
|
|
114
|
-
if (!ref && typeof o
|
|
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
|
|
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
|
|
133
|
-
const val = o
|
|
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
|
-
|
|
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
|
|
171
|
-
const val = o
|
|
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
|
-
|
|
203
|
+
const toolCalls = safeGet(s, "toolCalls");
|
|
204
|
+
if (Array.isArray(toolCalls)) {
|
|
188
205
|
const calls = (
|
|
189
206
|
/** @type {Array<Record<string, unknown>>} */
|
|
190
|
-
|
|
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
|
|
196
|
-
const result = call
|
|
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
|
-
|
|
219
|
+
const report = safeGet(r, "report");
|
|
220
|
+
if (report && typeof report === "object") {
|
|
203
221
|
return (
|
|
204
222
|
/** @type {Record<string, unknown>} */
|
|
205
|
-
|
|
223
|
+
report
|
|
206
224
|
);
|
|
207
225
|
}
|
|
208
226
|
}
|
|
209
227
|
}
|
|
210
228
|
}
|
|
211
229
|
}
|
|
212
|
-
|
|
230
|
+
const messages = safeGet(s, "messages");
|
|
231
|
+
if (Array.isArray(messages)) {
|
|
213
232
|
const msgs = (
|
|
214
233
|
/** @type {Array<Record<string, unknown>>} */
|
|
215
|
-
|
|
234
|
+
messages
|
|
216
235
|
);
|
|
217
236
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
218
237
|
const msg = msgs[i];
|
|
219
|
-
|
|
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
|
-
|
|
242
|
+
msgCalls
|
|
223
243
|
);
|
|
224
244
|
for (const call of calls) {
|
|
225
245
|
if (!call) continue;
|
|
226
|
-
const fn = call
|
|
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
|
|
252
|
+
if (String(safeGet(f, "name") ?? "").endsWith("iterate_review")) {
|
|
233
253
|
try {
|
|
234
|
-
const args = JSON.parse(String(f
|
|
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
|
|
253
|
-
return !!final && typeof final === "object" && (final
|
|
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
|
|
277
|
-
const val = o
|
|
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
|
-
|
|
313
|
+
const toolCalls = safeGet(s, "toolCalls");
|
|
314
|
+
if (Array.isArray(toolCalls)) {
|
|
294
315
|
const calls = (
|
|
295
316
|
/** @type {Array<Record<string, unknown>>} */
|
|
296
|
-
|
|
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
|
|
302
|
-
const found = findRunSummaryInObject(call
|
|
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
|
-
|
|
328
|
+
const messages = safeGet(s, "messages");
|
|
329
|
+
if (Array.isArray(messages)) {
|
|
308
330
|
const msgs = (
|
|
309
331
|
/** @type {Array<Record<string, unknown>>} */
|
|
310
|
-
|
|
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
|
|
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
|
|
351
|
+
safeGet(o, "finalReport")
|
|
330
352
|
);
|
|
331
|
-
const meta = final
|
|
353
|
+
const meta = safeGet(final, "metaReview") && typeof safeGet(final, "metaReview") === "object" ? (
|
|
332
354
|
/** @type {Record<string, unknown>} */
|
|
333
|
-
final
|
|
355
|
+
safeGet(final, "metaReview")
|
|
334
356
|
) : {};
|
|
335
|
-
const issues = Array.isArray(meta
|
|
336
|
-
const roundsVal = o
|
|
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
|
|
361
|
+
verdict: safeGet(final, "verdict") === "needs_revision" ? "needs_revision" : "approved",
|
|
340
362
|
reportIssues: issues.length,
|
|
341
|
-
checksRun: typeof meta
|
|
342
|
-
converged: o
|
|
363
|
+
checksRun: typeof safeGet(meta, "checksRun") === "number" ? safeGet(meta, "checksRun") : 0,
|
|
364
|
+
converged: safeGet(o, "converged") === true,
|
|
343
365
|
totalRounds,
|
|
344
|
-
totalFindings: typeof o
|
|
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
|
|
59
|
-
const data = /** @type {Record<string, unknown>} */ (direct
|
|
60
|
-
if (typeof data
|
|
61
|
-
best = data
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
84
|
-
const val = direct
|
|
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
|
|
119
|
-
ref = /** @type {Record<string, unknown>} */ (o
|
|
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
|
|
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
|
|
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
|
|
139
|
-
const val = o
|
|
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
|
|
164
|
-
|
|
165
|
-
Array.isArray(o
|
|
166
|
-
Array.isArray(o
|
|
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
|
|
205
|
-
const val = o
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
240
|
-
const result = call
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
330
|
+
const final = safeGet(o, 'finalReport')
|
|
300
331
|
return !!final &&
|
|
301
332
|
typeof final === 'object' &&
|
|
302
|
-
(final
|
|
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
|
|
334
|
-
const val = o
|
|
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
|
-
|
|
361
|
-
|
|
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
|
|
366
|
-
const found = findRunSummaryInObject(call
|
|
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
|
-
|
|
374
|
-
|
|
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
|
|
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
|
|
397
|
-
const meta = final
|
|
398
|
-
? /** @type {Record<string, unknown>} */ (final
|
|
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
|
|
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
|
|
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
|
|
438
|
+
verdict: safeGet(final, 'verdict') === 'needs_revision' ? 'needs_revision' : 'approved',
|
|
406
439
|
reportIssues: issues.length,
|
|
407
|
-
checksRun: typeof meta
|
|
408
|
-
converged: o
|
|
440
|
+
checksRun: typeof safeGet(meta, 'checksRun') === 'number' ? safeGet(meta, 'checksRun') : 0,
|
|
441
|
+
converged: safeGet(o, 'converged') === true,
|
|
409
442
|
totalRounds,
|
|
410
|
-
totalFindings: typeof o
|
|
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.
|
|
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",
|
package/src/config-loader.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
199
|
-
*
|
|
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()
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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({
|
package/src/tools/config.ts
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
}
|
package/src/tools/context.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 : ''
|
package/src/tools/history.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
package/src/tools/prune.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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)
|
package/src/tools/review.ts
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
}
|
package/src/tools/triage.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
}
|
package/src/tools/validate.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
loadEffectiveConfig,
|
|
5
5
|
isCommandAllowed,
|
|
6
6
|
flattenCommands,
|
|
7
|
-
|
|
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 =
|
|
129
|
+
async execute(args, exec) {
|
|
130
|
+
const resolved = resolveProjectRootForExec(exec, args.path)
|
|
131
131
|
if (!resolved.ok) {
|
|
132
132
|
return {
|
|
133
133
|
allowed: false,
|