iterate-plugin 3.4.1 → 3.5.0
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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/atomic-fs.js +77 -0
- package/dist/config-write.js +3 -2
- package/dist/live.js +5 -4
- package/dist/tools/checkpoint.js +10 -11
- package/dist/tools/config.js +14 -0
- package/dist/tools/context.js +3 -0
- package/dist/tools/decision-log.js +25 -10
- package/dist/tools/defense-events.js +3 -0
- package/dist/tools/defense-store.js +6 -2
- package/dist/tools/experience-bank.js +3 -0
- package/dist/tools/experience-store.js +6 -2
- package/dist/tools/fix.js +34 -4
- package/dist/tools/history.js +3 -0
- package/dist/tools/prune.js +50 -6
- package/dist/tools/quality-gate.js +3 -0
- package/dist/tools/quality-store.js +2 -1
- package/dist/tools/transcript.js +7 -5
- package/dist/tools/triage.js +6 -2
- package/dist/tools/validate.js +32 -3
- package/package.json +1 -1
- package/src/atomic-fs.ts +83 -0
- package/src/config-write.ts +3 -2
- package/src/live.ts +5 -4
- package/src/tools/checkpoint.ts +10 -11
- package/src/tools/config.ts +14 -0
- package/src/tools/context.ts +3 -0
- package/src/tools/decision-log.ts +35 -10
- package/src/tools/defense-events.ts +3 -0
- package/src/tools/defense-store.ts +6 -2
- package/src/tools/experience-bank.ts +3 -0
- package/src/tools/experience-store.ts +6 -2
- package/src/tools/fix.ts +32 -4
- package/src/tools/history.ts +3 -0
- package/src/tools/prune.ts +51 -6
- package/src/tools/quality-gate.ts +3 -0
- package/src/tools/quality-store.ts +2 -1
- package/src/tools/transcript.ts +7 -5
- package/src/tools/triage.ts +6 -2
- package/src/tools/validate.ts +32 -3
- package/src/types.ts +2 -0
|
@@ -59,6 +59,9 @@ function sanitizeRoundSeries(raw) {
|
|
|
59
59
|
export function registerQualityGateTool(ctx) {
|
|
60
60
|
ctx.tools.register(defineTool({
|
|
61
61
|
name: 'iterate_quality_gate',
|
|
62
|
+
// `read` never writes; `compute` persists the snapshot → only read
|
|
63
|
+
// joins a parallel dispatch group.
|
|
64
|
+
isConcurrencySafe: (args) => args.operation === 'read',
|
|
62
65
|
description: 'Query or write the quality gate status: dimension convergence rates, verification pass rates, ' +
|
|
63
66
|
'and overall PASS/FAIL status. ' +
|
|
64
67
|
'Operation "read" (default) returns the persisted machine-readable quality certificate. ' +
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
+
import { writeJsonAtomic } from "../atomic-fs.js";
|
|
10
11
|
const QUALITY_GATE_FILE = 'quality-gate.json';
|
|
11
12
|
/** Default empty quality gate snapshot. */
|
|
12
13
|
function emptySnapshot() {
|
|
@@ -96,7 +97,7 @@ export function writeQualityGate(projectRoot, snapshot) {
|
|
|
96
97
|
if (!fs.existsSync(dirPath)) {
|
|
97
98
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
+
writeJsonAtomic(filePath, snapshot);
|
|
100
101
|
}
|
|
101
102
|
catch (err) {
|
|
102
103
|
return { ok: false, error: `unable to write ${filePath}: ${String(err)}` };
|
package/dist/tools/transcript.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* tmp+rename so a crashed writer never leaves a corrupt manifest.
|
|
19
19
|
*/
|
|
20
20
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
21
|
-
import { mkdir, readFile
|
|
21
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
22
22
|
import { existsSync } from 'node:fs';
|
|
23
|
+
import { writeTextAtomicAsync } from "../atomic-fs.js";
|
|
23
24
|
import { dirname } from 'node:path';
|
|
24
25
|
import { loadEffectiveConfig, resolveProjectRootForExec, } from "../config-loader.js";
|
|
25
26
|
import { transcriptPath } from "../paths.js";
|
|
@@ -96,6 +97,9 @@ function normalizeFix(input) {
|
|
|
96
97
|
export function registerTranscriptTool(ctx) {
|
|
97
98
|
ctx.tools.register(defineTool({
|
|
98
99
|
name: 'iterate_transcript',
|
|
100
|
+
// `read` never writes; `capture`/`nudge` persist transcript state → only
|
|
101
|
+
// read joins a parallel dispatch group.
|
|
102
|
+
isConcurrencySafe: (args) => args.operation === 'read',
|
|
99
103
|
description: 'Runtime-observatory transcript for the iterate workflow. ' +
|
|
100
104
|
'`read` returns the current persisted transcript manifest (per-reviewer threads, ' +
|
|
101
105
|
'convergence series, findings, fixes, checkpoint, timeline, and any steering nudge ' +
|
|
@@ -322,10 +326,8 @@ function rehydrateBuilder(manifest, approval) {
|
|
|
322
326
|
builder.finish();
|
|
323
327
|
return builder;
|
|
324
328
|
}
|
|
325
|
-
/** Atomically persist a manifest (
|
|
329
|
+
/** Atomically persist a manifest (unique temp + rename) under `.iterate/`. */
|
|
326
330
|
async function persist(file, manifest) {
|
|
327
331
|
await mkdir(dirname(file), { recursive: true });
|
|
328
|
-
|
|
329
|
-
await writeFile(tmp, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
330
|
-
await rename(tmp, file);
|
|
332
|
+
await writeTextAtomicAsync(file, JSON.stringify(manifest, null, 2));
|
|
331
333
|
}
|
package/dist/tools/triage.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, readFileSync, rmSync
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, rmSync } 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
5
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
6
|
+
import { writeTextAtomic } from "../atomic-fs.js";
|
|
6
7
|
const CONFIG_FILE = 'iterate.config.yaml';
|
|
7
8
|
/** Personalization key that holds the known-intentional list. */
|
|
8
9
|
const PERSONALIZATION_KEY = 'personalization';
|
|
@@ -191,7 +192,7 @@ function applyEntries(projectRoot, incoming) {
|
|
|
191
192
|
}
|
|
192
193
|
const yamlText = yaml.dump(nextConfig, { noRefs: true });
|
|
193
194
|
try {
|
|
194
|
-
|
|
195
|
+
writeTextAtomic(configPath, yamlText);
|
|
195
196
|
}
|
|
196
197
|
catch (err) {
|
|
197
198
|
// Rollback: restore the backup, or REMOVE the file we just created when
|
|
@@ -231,6 +232,9 @@ function applyEntries(projectRoot, incoming) {
|
|
|
231
232
|
export function registerTriageTool(ctx) {
|
|
232
233
|
ctx.tools.register(defineTool({
|
|
233
234
|
name: 'iterate_triage',
|
|
235
|
+
// `list` is a pure config read; `apply` rewrites iterate.config.yaml →
|
|
236
|
+
// only list joins a parallel dispatch group.
|
|
237
|
+
isConcurrencySafe: (args) => args.operation === 'list',
|
|
234
238
|
description: 'Manage `personalization.known_intentional` entries in iterate.config.yaml. ' +
|
|
235
239
|
'Use `apply` to write back triage verdicts (entries where the reviewer said "known intentional") so ' +
|
|
236
240
|
'future review rounds filter them out. Entries are deduped by file|dimension|line and the config is ' +
|
package/dist/tools/validate.js
CHANGED
|
@@ -19,8 +19,14 @@ export function clampTimeout(ms) {
|
|
|
19
19
|
/**
|
|
20
20
|
* Run a single shell command with timeout and return structured results.
|
|
21
21
|
* Pure function (no side effects beyond the exec call).
|
|
22
|
+
*
|
|
23
|
+
* `signal` (the caller's `exec.signal` per the dsh tools contract) is forwarded
|
|
24
|
+
* to the child process so a cancelled tool call kills the running command
|
|
25
|
+
* instead of pinning the dispatch open until the timeout elapses. The result
|
|
26
|
+
* distinguishes "cancelled by the caller" (`canceled: true`) from "ran past
|
|
27
|
+
* `timeoutMs`" (`timedOut: true`).
|
|
22
28
|
*/
|
|
23
|
-
async function runCommand(command, cwd, timeoutMs) {
|
|
29
|
+
export async function runCommand(command, cwd, timeoutMs, signal) {
|
|
24
30
|
const start = performance.now();
|
|
25
31
|
return new Promise((resolve) => {
|
|
26
32
|
exec(command, {
|
|
@@ -28,18 +34,23 @@ async function runCommand(command, cwd, timeoutMs) {
|
|
|
28
34
|
timeout: timeoutMs,
|
|
29
35
|
maxBuffer: 10 * 1024 * 1024, // 10 MB
|
|
30
36
|
env: { ...process.env, PAGER: 'cat' },
|
|
37
|
+
signal,
|
|
31
38
|
}, (error, stdout, stderr) => {
|
|
32
39
|
const durationMs = Math.round(performance.now() - start);
|
|
33
40
|
// error.code is the exit code when the command ran; when the binary
|
|
34
41
|
// cannot be spawned Node sets error.code to a STRING ('ENOENT' etc).
|
|
35
42
|
// Coerce to a number so the integer output schema is never violated.
|
|
36
43
|
const exitCode = typeof error?.code === 'number' ? error.code : (error ? 1 : 0);
|
|
44
|
+
const canceled = signal?.aborted === true;
|
|
37
45
|
resolve({
|
|
38
46
|
command,
|
|
39
47
|
exitCode,
|
|
40
48
|
stdout: stdout ?? '',
|
|
41
49
|
stderr: stderr ?? '',
|
|
42
|
-
|
|
50
|
+
// A caller cancellation also kills the child (killed === true), but
|
|
51
|
+
// that is not a timeout — only report timedOut for the deadline path.
|
|
52
|
+
timedOut: error?.killed === true && !canceled,
|
|
53
|
+
canceled,
|
|
43
54
|
durationMs,
|
|
44
55
|
});
|
|
45
56
|
});
|
|
@@ -53,6 +64,19 @@ async function runCommand(command, cwd, timeoutMs) {
|
|
|
53
64
|
export function registerValidateTool(ctx) {
|
|
54
65
|
ctx.tools.register(defineTool({
|
|
55
66
|
name: 'iterate_validate',
|
|
67
|
+
// Pending-call card: render the pending validation as a terminal card.
|
|
68
|
+
// Pure — derived from args only; no `cwd` (the UI bridge resolves the
|
|
69
|
+
// relative command against the session workspace).
|
|
70
|
+
presentCall: (args) => {
|
|
71
|
+
const a = args;
|
|
72
|
+
if (typeof a.command !== 'string' || a.command.length === 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
card: 'terminal',
|
|
76
|
+
title: a.command,
|
|
77
|
+
description: 'Run a preconfigured iterate validation command (exact match on validation.commands).',
|
|
78
|
+
};
|
|
79
|
+
},
|
|
56
80
|
description: 'Run a validation command that is PRECONFIGURED in iterate.config.yaml `validation.commands`. ' +
|
|
57
81
|
'The command must exactly match one of the configured commands (they are the only ones the user trusts). ' +
|
|
58
82
|
'Returns exit code, stdout, stderr, and duration. ' +
|
|
@@ -83,6 +107,7 @@ export function registerValidateTool(ctx) {
|
|
|
83
107
|
stdout: { type: 'string', required: true },
|
|
84
108
|
stderr: { type: 'string', required: true },
|
|
85
109
|
timedOut: { type: 'boolean', required: true },
|
|
110
|
+
canceled: { type: 'boolean', required: true },
|
|
86
111
|
durationMs: { type: 'integer', required: true },
|
|
87
112
|
rejectReason: { type: 'string' },
|
|
88
113
|
},
|
|
@@ -96,6 +121,7 @@ export function registerValidateTool(ctx) {
|
|
|
96
121
|
`Exit code: ${value.exitCode}`,
|
|
97
122
|
`Duration: ${value.durationMs}ms`,
|
|
98
123
|
value.timedOut ? '⚠ Timed out' : '',
|
|
124
|
+
value.canceled ? '⚠ Cancelled before completion' : '',
|
|
99
125
|
'',
|
|
100
126
|
value.stdout ? `[stdout]\n${value.stdout}` : '',
|
|
101
127
|
value.stderr ? `[stderr]\n${value.stderr}` : '',
|
|
@@ -116,6 +142,7 @@ export function registerValidateTool(ctx) {
|
|
|
116
142
|
stdout: '',
|
|
117
143
|
stderr: '',
|
|
118
144
|
timedOut: false,
|
|
145
|
+
canceled: false,
|
|
119
146
|
durationMs: 0,
|
|
120
147
|
rejectReason: resolved.reason,
|
|
121
148
|
};
|
|
@@ -137,6 +164,7 @@ export function registerValidateTool(ctx) {
|
|
|
137
164
|
stdout: '',
|
|
138
165
|
stderr: '',
|
|
139
166
|
timedOut: false,
|
|
167
|
+
canceled: false,
|
|
140
168
|
durationMs: 0,
|
|
141
169
|
rejectReason: (source === 'defaults'
|
|
142
170
|
? 'No iterate.config.yaml at project root — running on built-in defaults, which configure NO trusted validation commands. '
|
|
@@ -152,12 +180,13 @@ export function registerValidateTool(ctx) {
|
|
|
152
180
|
stdout: '',
|
|
153
181
|
stderr: '',
|
|
154
182
|
timedOut: false,
|
|
183
|
+
canceled: false,
|
|
155
184
|
durationMs: 0,
|
|
156
185
|
rejectReason: `Command must exactly match a command predefined in iterate.config.yaml validation.commands. ` +
|
|
157
186
|
`Allowed commands: ${predefinedCommands.join(' | ')}`,
|
|
158
187
|
};
|
|
159
188
|
}
|
|
160
|
-
const result = await runCommand(args.command, projectRoot, timeout);
|
|
189
|
+
const result = await runCommand(args.command, projectRoot, timeout, exec.signal);
|
|
161
190
|
return {
|
|
162
191
|
allowed: true,
|
|
163
192
|
...result,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/atomic-fs.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/atomic-fs.ts — crash-safe file writes for all plugin state.
|
|
3
|
+
*
|
|
4
|
+
* Every JSON/YAML artifact the plugin persists (.iterate/*.json,
|
|
5
|
+
* iterate.config.yaml) is written through here: content goes to a
|
|
6
|
+
* uniquely-named temp file in the SAME directory as the target (rename is
|
|
7
|
+
* only atomic within one filesystem), then is `renameSync`ed over the
|
|
8
|
+
* destination. A crash mid-write can therefore leave a stale temp file
|
|
9
|
+
* behind, but never a truncated / half-written state file — readers
|
|
10
|
+
* (which all tolerate missing/corrupt files) see either the previous or
|
|
11
|
+
* the new content, never a mixture.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
import { renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { rename, rm, writeFile } from 'node:fs/promises'
|
|
17
|
+
|
|
18
|
+
/** Best-effort unlink of a leftover temp file (never throws). */
|
|
19
|
+
function cleanupTemp(tmp: string): void {
|
|
20
|
+
try {
|
|
21
|
+
rmSync(tmp, { force: true })
|
|
22
|
+
} catch {
|
|
23
|
+
/* ignore — temp files follow the prunable `.tmp-<pid>-<rand>` convention */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Best-effort async unlink of a leftover temp file (never throws). */
|
|
28
|
+
async function cleanupTempAsync(tmp: string): Promise<void> {
|
|
29
|
+
try {
|
|
30
|
+
await rm(tmp, { force: true })
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore — temp files follow the prunable `.tmp-<pid>-<rand>` convention */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unique temp file path for `filePath`, in the same directory, following the
|
|
38
|
+
* prunable `.<basename>.tmp-<pid>-<random>` convention (see prune.ts
|
|
39
|
+
* `isPrunableTemp`). The pid+random suffix makes concurrent writers safe.
|
|
40
|
+
*/
|
|
41
|
+
function tempPathFor(filePath: string): string {
|
|
42
|
+
return join(
|
|
43
|
+
dirname(filePath),
|
|
44
|
+
`.${filePath.split('/').pop() ?? 'file'}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write `data` to `filePath` atomically (temp file + rename, same dir).
|
|
50
|
+
* Temp files follow the `<name>.tmp-<pid>-<random>` convention also used by
|
|
51
|
+
* prune.ts / checkpoint.ts so they are recognizable and prunable.
|
|
52
|
+
* Throws on failure (callers own error reporting — they map failures to
|
|
53
|
+
* structured tool results).
|
|
54
|
+
*/
|
|
55
|
+
export function writeTextAtomic(filePath: string, data: string): void {
|
|
56
|
+
const tmp = tempPathFor(filePath)
|
|
57
|
+
writeFileSync(tmp, data, 'utf-8')
|
|
58
|
+
try {
|
|
59
|
+
renameSync(tmp, filePath)
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Best-effort temp cleanup so a failed write never litters the directory.
|
|
62
|
+
cleanupTemp(tmp)
|
|
63
|
+
throw err
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Async variant of {@link writeTextAtomic} (same temp naming convention). */
|
|
68
|
+
export async function writeTextAtomicAsync(filePath: string, data: string): Promise<void> {
|
|
69
|
+
const tmp = tempPathFor(filePath)
|
|
70
|
+
await writeFile(tmp, data, 'utf-8')
|
|
71
|
+
try {
|
|
72
|
+
await rename(tmp, filePath)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Best-effort temp cleanup so a failed write never litters the directory.
|
|
75
|
+
await cleanupTempAsync(tmp)
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** JSON.stringify (2-space indent) + atomic write in one call. */
|
|
81
|
+
export function writeJsonAtomic(filePath: string, value: unknown): void {
|
|
82
|
+
writeTextAtomic(filePath, JSON.stringify(value, null, 2))
|
|
83
|
+
}
|
package/src/config-write.ts
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* config, always back up before writing, roll back on failure.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { copyFileSync, existsSync, readFileSync, rmSync
|
|
13
|
+
import { copyFileSync, existsSync, readFileSync, rmSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
15
|
import yaml from 'js-yaml'
|
|
16
|
+
import { writeTextAtomic } from './atomic-fs.ts'
|
|
16
17
|
|
|
17
18
|
/** Config file name (must match config-loader). */
|
|
18
19
|
export const CONFIG_FILE = 'iterate.config.yaml'
|
|
@@ -194,7 +195,7 @@ export function writeConfigFile(
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
try {
|
|
197
|
-
|
|
198
|
+
writeTextAtomic(configPath, yaml.dump(config, { noRefs: true }))
|
|
198
199
|
} catch (err) {
|
|
199
200
|
let rollbackError = ''
|
|
200
201
|
try {
|
package/src/live.ts
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* or crash a tool call.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { mkdir, readFile,
|
|
27
|
+
import { mkdir, readFile, stat, appendFile } from 'node:fs/promises'
|
|
28
28
|
import { existsSync } from 'node:fs'
|
|
29
|
+
import { writeTextAtomicAsync } from './atomic-fs.ts'
|
|
29
30
|
import { join } from 'node:path'
|
|
30
31
|
import type { Context } from '@deepseek-ai/cordis'
|
|
31
32
|
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
|
@@ -136,9 +137,9 @@ export async function appendLive(projectRoot: string, entry: LiveActivityEntry):
|
|
|
136
137
|
const raw = await readFile(file, 'utf-8')
|
|
137
138
|
const lines = raw.split('\n').filter(Boolean)
|
|
138
139
|
const tail = lines.slice(-LIVE_MAX_ENTRIES)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await
|
|
140
|
+
// Atomic rewrite (unique temp + rename) so a crash mid-trim never
|
|
141
|
+
// truncates the live feed; the temp file is prunable by iterate_prune.
|
|
142
|
+
await writeTextAtomicAsync(file, tail.join('\n') + '\n')
|
|
142
143
|
}
|
|
143
144
|
await appendFile(file, line, 'utf-8')
|
|
144
145
|
} catch {
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* Checkpoint layout: `.iterate/checkpoint.json`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
13
13
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
14
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
15
15
|
import { resolveProjectRootForExec } from '../config-loader.ts'
|
|
16
|
+
import { writeJsonAtomic } from '../atomic-fs.ts'
|
|
16
17
|
import { checkpointPath, iterateDir, transcriptPath } from '../paths.ts'
|
|
17
18
|
import { readRegistry } from './fix.ts'
|
|
18
19
|
import { readDecisionEntries } from './decision-log.ts'
|
|
@@ -223,10 +224,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
223
224
|
}
|
|
224
225
|
try {
|
|
225
226
|
mkdirSync(iterateDir(projectRoot), { recursive: true })
|
|
226
|
-
|
|
227
|
-
const tmpPath = `${cpPath}.tmp-${Date.now()}`
|
|
228
|
-
writeFileSync(tmpPath, JSON.stringify(resumed, null, 2), 'utf-8')
|
|
229
|
-
renameSync(tmpPath, cpPath)
|
|
227
|
+
writeJsonAtomic(checkpointPath(projectRoot), resumed)
|
|
230
228
|
} catch (err) {
|
|
231
229
|
return { operation: 'resume', ok: false, error: `failed to persist resumed checkpoint: ${String(err)}` }
|
|
232
230
|
}
|
|
@@ -266,12 +264,10 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
266
264
|
}
|
|
267
265
|
try {
|
|
268
266
|
mkdirSync(iterateDir(projectRoot), { recursive: true })
|
|
269
|
-
// Atomic write (temp + rename): a crash
|
|
270
|
-
// the checkpoint and silently lose the
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
writeFileSync(tmpPath, JSON.stringify(checkpoint, null, 2), 'utf-8')
|
|
274
|
-
renameSync(tmpPath, cpPath)
|
|
267
|
+
// Atomic write (temp + rename, via writeJsonAtomic): a crash
|
|
268
|
+
// mid-write must not corrupt the checkpoint and silently lose the
|
|
269
|
+
// interruption state.
|
|
270
|
+
writeJsonAtomic(checkpointPath(projectRoot), checkpoint)
|
|
275
271
|
} catch (err) {
|
|
276
272
|
return { operation: 'save', ok: false, error: `failed to write checkpoint: ${String(err)}` }
|
|
277
273
|
}
|
|
@@ -294,6 +290,9 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
294
290
|
ctx.tools.register(
|
|
295
291
|
defineTool({
|
|
296
292
|
name: 'iterate_status',
|
|
293
|
+
// Read-only aggregation of on-disk state → safe to join a parallel
|
|
294
|
+
// dispatch group alongside other read-only sibling calls.
|
|
295
|
+
isConcurrencySafe: () => true,
|
|
297
296
|
description:
|
|
298
297
|
'Summarize the current iterate run: mode, current round vs total, fixes applied, architectural ' +
|
|
299
298
|
'findings remaining, decision-log size, and whether a resume checkpoint exists.',
|
package/src/tools/config.ts
CHANGED
|
@@ -19,6 +19,20 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
19
19
|
ctx.tools.register(
|
|
20
20
|
defineTool({
|
|
21
21
|
name: 'iterate_config',
|
|
22
|
+
// `read` never writes; `write` persists a config update → only read
|
|
23
|
+
// joins a parallel dispatch group.
|
|
24
|
+
isConcurrencySafe: (args) => (args as { operation?: unknown }).operation !== 'write',
|
|
25
|
+
// Pending-call card: a config read vs a validated write.
|
|
26
|
+
presentCall: (args) => {
|
|
27
|
+
const a = args as { operation?: unknown; section?: unknown }
|
|
28
|
+
const isWrite = a.operation === 'write'
|
|
29
|
+
return {
|
|
30
|
+
card: 'generic',
|
|
31
|
+
title: isWrite ? 'Update iterate.config.yaml' : 'Read iterate.config.yaml',
|
|
32
|
+
kind: isWrite ? 'edit' : 'read',
|
|
33
|
+
...(typeof a.section === 'string' && a.section ? { rawInput: { section: a.section } } : {}),
|
|
34
|
+
}
|
|
35
|
+
},
|
|
22
36
|
description:
|
|
23
37
|
'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
24
38
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
package/src/tools/context.ts
CHANGED
|
@@ -178,6 +178,9 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
178
178
|
ctx.tools.register(
|
|
179
179
|
defineTool({
|
|
180
180
|
name: 'iterate_context',
|
|
181
|
+
// Read-only (reads SKILL.md / ITERATE.md) → safe to join a parallel
|
|
182
|
+
// dispatch group alongside other read-only sibling calls.
|
|
183
|
+
isConcurrencySafe: () => true,
|
|
181
184
|
description:
|
|
182
185
|
'Read project context files (SKILL.md and/or ITERATE.md). ' +
|
|
183
186
|
'SKILL.md contains the original iterate skill instructions; it is searched in ' +
|
|
@@ -77,30 +77,50 @@ export function appendDecisionEntry(projectRoot: string, entry: DecisionLogEntry
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
80
|
+
* Detailed log read: valid entries plus the number of corrupt lines that were
|
|
81
|
+
* skipped, so callers can surface silent audit-trail loss to the model/UI.
|
|
82
|
+
*/
|
|
83
|
+
export interface DecisionLogRead {
|
|
84
|
+
entries: DecisionLogEntry[]
|
|
85
|
+
invalidLines: number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read all entries from the decision log, plus the count of corrupt lines.
|
|
81
90
|
* A single corrupt line (partial write, hand-edit) is SKIPPED, not fatal —
|
|
82
|
-
* one bad line must never empty the whole history for every reader
|
|
91
|
+
* one bad line must never empty the whole history for every reader — but it
|
|
92
|
+
* is counted in `invalidLines` so it never disappears silently.
|
|
83
93
|
*/
|
|
84
|
-
export function
|
|
94
|
+
export function readDecisionLogDetailed(projectRoot: string): DecisionLogRead {
|
|
85
95
|
const filePath = join(projectRoot, LOG_DIR, LOG_FILE)
|
|
86
|
-
if (!existsSync(filePath)) return []
|
|
96
|
+
if (!existsSync(filePath)) return { entries: [], invalidLines: 0 }
|
|
87
97
|
let content: string
|
|
88
98
|
try {
|
|
89
99
|
content = readFileSync(filePath, 'utf-8')
|
|
90
100
|
} catch {
|
|
91
|
-
return []
|
|
101
|
+
return { entries: [], invalidLines: 0 }
|
|
92
102
|
}
|
|
93
|
-
const
|
|
103
|
+
const entries: DecisionLogEntry[] = []
|
|
104
|
+
let invalidLines = 0
|
|
94
105
|
for (const line of content.split('\n')) {
|
|
95
106
|
const trimmed = line.trim()
|
|
96
107
|
if (trimmed.length === 0) continue
|
|
97
108
|
try {
|
|
98
|
-
|
|
109
|
+
entries.push(JSON.parse(trimmed) as DecisionLogEntry)
|
|
99
110
|
} catch {
|
|
100
|
-
|
|
111
|
+
invalidLines += 1
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
|
-
return
|
|
114
|
+
return { entries, invalidLines }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read all entries from the decision log.
|
|
119
|
+
* A single corrupt line (partial write, hand-edit) is SKIPPED, not fatal —
|
|
120
|
+
* one bad line must never empty the whole history for every reader.
|
|
121
|
+
*/
|
|
122
|
+
export function readDecisionEntries(projectRoot: string): DecisionLogEntry[] {
|
|
123
|
+
return readDecisionLogDetailed(projectRoot).entries
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
/**
|
|
@@ -112,6 +132,9 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
112
132
|
ctx.tools.register(
|
|
113
133
|
defineTool({
|
|
114
134
|
name: 'iterate_decision_log',
|
|
135
|
+
// `read` never writes; `append` extends the audit log → only read joins
|
|
136
|
+
// a parallel dispatch group.
|
|
137
|
+
isConcurrencySafe: (args) => (args as { operation?: unknown }).operation === 'read',
|
|
115
138
|
description:
|
|
116
139
|
'Append-only decision log for the iterate loop. ' +
|
|
117
140
|
'Use `append` to record a round start, review finding, fix, validation result, or decision. ' +
|
|
@@ -164,6 +187,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
164
187
|
properties: {
|
|
165
188
|
operation: { type: 'string', required: true },
|
|
166
189
|
entryCount: { type: 'integer' },
|
|
190
|
+
invalidLines: { type: 'integer' },
|
|
167
191
|
logPath: { type: 'string' },
|
|
168
192
|
entries: { type: 'json' },
|
|
169
193
|
success: { type: 'boolean' },
|
|
@@ -184,10 +208,11 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
184
208
|
const projectRoot = resolved.root
|
|
185
209
|
|
|
186
210
|
if (args.operation === 'read') {
|
|
187
|
-
const entries =
|
|
211
|
+
const { entries, invalidLines } = readDecisionLogDetailed(projectRoot)
|
|
188
212
|
return {
|
|
189
213
|
operation: 'read',
|
|
190
214
|
entryCount: entries.length,
|
|
215
|
+
invalidLines,
|
|
191
216
|
logPath: join(projectRoot, LOG_DIR, LOG_FILE),
|
|
192
217
|
entries: entries as unknown as JsonValue,
|
|
193
218
|
}
|
|
@@ -91,6 +91,9 @@ export function registerDefenseEventsTool(ctx: { tools: { register: (def: Return
|
|
|
91
91
|
ctx.tools.register(
|
|
92
92
|
defineTool({
|
|
93
93
|
name: 'iterate_defense_events',
|
|
94
|
+
// List/counts never write; only `record` persists an event → read shapes
|
|
95
|
+
// join a parallel dispatch group.
|
|
96
|
+
isConcurrencySafe: (args) => (args as { operation?: unknown }).operation !== 'record',
|
|
94
97
|
description:
|
|
95
98
|
'Query or record defense events: precondition failures, rollbacks, invariant violations, ' +
|
|
96
99
|
'and assumption falsifications. ' +
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from 'node:fs'
|
|
9
9
|
import * as path from 'node:path'
|
|
10
|
+
import { iterateDir } from '../paths.ts'
|
|
11
|
+
import { writeJsonAtomic } from '../atomic-fs.ts'
|
|
10
12
|
import type { DefenseEvent, DefenseEventStream, DefenseEventType } from '../types.ts'
|
|
11
13
|
|
|
12
14
|
const DEFENSE_EVENTS_FILE = 'defense-events.json'
|
|
@@ -83,14 +85,16 @@ export function writeDefenseEvents(
|
|
|
83
85
|
projectRoot: string,
|
|
84
86
|
stream: DefenseEventStream,
|
|
85
87
|
): { ok: true } | { ok: false; error: string } {
|
|
86
|
-
const dirPath =
|
|
88
|
+
const dirPath = iterateDir(projectRoot)
|
|
87
89
|
const filePath = path.join(dirPath, DEFENSE_EVENTS_FILE)
|
|
88
90
|
|
|
89
91
|
try {
|
|
90
92
|
if (!fs.existsSync(dirPath)) {
|
|
91
93
|
fs.mkdirSync(dirPath, { recursive: true })
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
// Atomic (temp + rename): a crash mid-write can never leave a truncated
|
|
96
|
+
// defense event stream behind.
|
|
97
|
+
writeJsonAtomic(filePath, stream)
|
|
94
98
|
} catch (err) {
|
|
95
99
|
return { ok: false, error: `unable to write ${filePath}: ${String(err)}` }
|
|
96
100
|
}
|
|
@@ -72,6 +72,9 @@ export function registerExperienceBankTool(ctx: { tools: { register: (def: Retur
|
|
|
72
72
|
ctx.tools.register(
|
|
73
73
|
defineTool({
|
|
74
74
|
name: 'iterate_experience',
|
|
75
|
+
// List/search/get never write; only `add` upserts the bank → read shapes
|
|
76
|
+
// join a parallel dispatch group.
|
|
77
|
+
isConcurrencySafe: (args) => (args as { operation?: unknown }).operation !== 'add',
|
|
75
78
|
description:
|
|
76
79
|
'Query or extend the experience bank: browse/search historical fixes and patterns, ' +
|
|
77
80
|
'or record a new verified fix (operation:"add"). ' +
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from 'node:fs'
|
|
9
9
|
import * as path from 'node:path'
|
|
10
|
+
import { iterateDir } from '../paths.ts'
|
|
11
|
+
import { writeJsonAtomic } from '../atomic-fs.ts'
|
|
10
12
|
import type { ExperienceBank, ExperienceEntry } from '../types.ts'
|
|
11
13
|
|
|
12
14
|
const EXPERIENCE_FILE = 'experience.json'
|
|
@@ -45,14 +47,16 @@ export function writeExperienceBank(
|
|
|
45
47
|
projectRoot: string,
|
|
46
48
|
bank: ExperienceBank,
|
|
47
49
|
): { ok: true } | { ok: false; error: string } {
|
|
48
|
-
const dirPath =
|
|
50
|
+
const dirPath = iterateDir(projectRoot)
|
|
49
51
|
const filePath = path.join(dirPath, EXPERIENCE_FILE)
|
|
50
52
|
|
|
51
53
|
try {
|
|
52
54
|
if (!fs.existsSync(dirPath)) {
|
|
53
55
|
fs.mkdirSync(dirPath, { recursive: true })
|
|
54
56
|
}
|
|
55
|
-
|
|
57
|
+
// Atomic (temp + rename): a crash mid-write can never leave a truncated
|
|
58
|
+
// experience bank behind.
|
|
59
|
+
writeJsonAtomic(filePath, bank)
|
|
56
60
|
} catch (err) {
|
|
57
61
|
return { ok: false, error: `unable to write ${filePath}: ${String(err)}` }
|
|
58
62
|
}
|