iterate-plugin 2.11.0 → 2.12.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 +41 -1
- package/README.zh-CN.md +39 -1
- package/dist/approval-gate.js +92 -0
- package/dist/config-loader.js +4 -0
- package/dist/index.js +15 -5
- package/dist/live.js +155 -0
- package/dist/paths.js +4 -0
- package/dist/session-hooks.js +89 -0
- package/dist/skill-prompt.js +56 -4
- package/dist/tools/transcript.js +324 -0
- package/dist/transcript.js +421 -0
- package/lib/client.js +814 -0
- package/lib/parse.js +276 -0
- package/package.json +1 -1
- package/src/approval-gate.ts +119 -0
- package/src/client/index.ts +663 -6
- package/src/config-loader.ts +4 -0
- package/src/index.ts +17 -6
- package/src/live.ts +185 -0
- package/src/paths.ts +5 -0
- package/src/session-hooks.ts +90 -0
- package/src/skill-prompt.ts +56 -4
- package/src/tools/transcript.ts +334 -0
- package/src/transcript.ts +475 -0
- package/src/types.ts +117 -0
package/src/config-loader.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
|
|
3
3
|
*
|
|
4
4
|
* Architecture:
|
|
5
|
-
* - The plugin registers
|
|
6
|
-
* triage, fix, diff, rollback, checkpoint, status, history, prune)
|
|
5
|
+
* - The plugin registers 14 tools (config, validate, decision-log, context, review,
|
|
6
|
+
* triage, fix, diff, rollback, checkpoint, status, history, prune, transcript)
|
|
7
7
|
* - The plugin injects a system prompt section teaching the iterate workflow pattern
|
|
8
8
|
* - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
|
|
9
9
|
* - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
|
|
10
|
-
* - Subagents use the
|
|
11
|
-
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune)
|
|
10
|
+
* - Subagents use the 14 tools to do real work (read config, run validation, log decisions,
|
|
11
|
+
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript)
|
|
12
|
+
* - A `tools/pre-execute` hook gates destructive iterate calls behind human approval
|
|
13
|
+
* (F8 observatory approval policy: ask / deny / allow).
|
|
12
14
|
*
|
|
13
15
|
* Tool invocation model:
|
|
14
16
|
* - Workflow script CANNOT call tools directly (sandboxed vm, no Node API)
|
|
@@ -34,13 +36,16 @@ import { registerFixTool, registerDiffTool, registerRollbackTool } from './tools
|
|
|
34
36
|
import { registerCheckpointTool, registerStatusTool } from './tools/checkpoint.ts'
|
|
35
37
|
import { registerHistoryTool } from './tools/history.ts'
|
|
36
38
|
import { registerPruneTool } from './tools/prune.ts'
|
|
39
|
+
import { registerTranscriptTool } from './tools/transcript.ts'
|
|
40
|
+
import { registerSessionHooks } from './session-hooks.ts'
|
|
41
|
+
import { registerLiveCapture } from './live.ts'
|
|
37
42
|
import { ITERATE_SKILL_PROMPT } from './skill-prompt.ts'
|
|
38
43
|
|
|
39
44
|
export const name = 'iterate-plugin'
|
|
40
|
-
export const inject = ['tools', 'systemPrompt']
|
|
45
|
+
export const inject = ['tools', 'systemPrompt'] as const
|
|
41
46
|
|
|
42
47
|
export function apply(ctx: Context): void {
|
|
43
|
-
// 1. Register the
|
|
48
|
+
// 1. Register the 14 tools
|
|
44
49
|
registerConfigTool(ctx)
|
|
45
50
|
registerValidateTool(ctx)
|
|
46
51
|
registerDecisionLogTool(ctx)
|
|
@@ -54,6 +59,12 @@ export function apply(ctx: Context): void {
|
|
|
54
59
|
registerStatusTool(ctx)
|
|
55
60
|
registerHistoryTool(ctx)
|
|
56
61
|
registerPruneTool(ctx)
|
|
62
|
+
registerTranscriptTool(ctx)
|
|
63
|
+
|
|
64
|
+
// 2. Wire the observatory approval gate onto dsh's tools/pre-execute waterfall,
|
|
65
|
+
// and the live reviewer-activity feed onto tools/result.
|
|
66
|
+
registerSessionHooks(ctx)
|
|
67
|
+
registerLiveCapture(ctx)
|
|
57
68
|
|
|
58
69
|
// 2. Inject the iterate skill prompt as a system prompt section
|
|
59
70
|
// This teaches the model how to write iterate workflow scripts using the tools.
|
package/src/live.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/live.ts — live reviewer-activity feed for the iterate observatory (F1 live).
|
|
3
|
+
*
|
|
4
|
+
* Watches `tools/result` and, for tool calls we can attribute to a project root
|
|
5
|
+
* (the caller agent's session cwd), appends one line to an append-only NDJSON
|
|
6
|
+
* file `.iterate/transcript-live.ndjson`. The `iterate_transcript` tool then
|
|
7
|
+
* mixes the most recent entries into its `read` / `capture` results so the
|
|
8
|
+
* client observatory shows what reviewers are doing in near-real-time (which
|
|
9
|
+
* files they read, which fixes/rollbacks/diffs land, where the run is).
|
|
10
|
+
*
|
|
11
|
+
* Why project-scoped (not per-thread):
|
|
12
|
+
* Tool executions carry the calling agent's session cwd but NOT the workflow
|
|
13
|
+
* sub-agent's `dimension` / `round` label, so we cannot reliably attribute a
|
|
14
|
+
* read to a specific reviewer thread without inventing data. We therefore
|
|
15
|
+
* record honest project-level activity and never fabricate an attribution.
|
|
16
|
+
* Per-thread narration stays the job of the final `iterate_transcript capture`.
|
|
17
|
+
*
|
|
18
|
+
* Safety:
|
|
19
|
+
* - Read-only observer: never mutates source files; writes only the NDJSON
|
|
20
|
+
* live file under `.iterate/`.
|
|
21
|
+
* - The live file is byte-capped (rewrite to last N lines when it grows too
|
|
22
|
+
* large) so it can never grow unbounded.
|
|
23
|
+
* - Any capture failure is swallowed (fire-and-forget) so it can never block
|
|
24
|
+
* or crash a tool call.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { mkdir, readFile, writeFile, stat, appendFile, rename } from 'node:fs/promises'
|
|
28
|
+
import { existsSync } from 'node:fs'
|
|
29
|
+
import { join } from 'node:path'
|
|
30
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
31
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
|
32
|
+
import { resolveProjectRoot } from './config-loader.ts'
|
|
33
|
+
|
|
34
|
+
/** Keep at most this many live activity entries. */
|
|
35
|
+
export const LIVE_MAX_ENTRIES = 300
|
|
36
|
+
/** Rewrite the live file when its byte size exceeds this threshold. */
|
|
37
|
+
export const LIVE_MAX_BYTES = 64 * 1024
|
|
38
|
+
|
|
39
|
+
/** One live activity record. */
|
|
40
|
+
export interface LiveActivityEntry {
|
|
41
|
+
/** ISO 8601 timestamp of the tool result. */
|
|
42
|
+
ts: string
|
|
43
|
+
/** Coarse category used by the client for coloring/grouping. */
|
|
44
|
+
type:
|
|
45
|
+
| 'read'
|
|
46
|
+
| 'fix'
|
|
47
|
+
| 'rollback'
|
|
48
|
+
| 'diff'
|
|
49
|
+
| 'review'
|
|
50
|
+
| 'triage'
|
|
51
|
+
| 'checkpoint'
|
|
52
|
+
| 'validate'
|
|
53
|
+
| 'log'
|
|
54
|
+
| 'prune'
|
|
55
|
+
| 'info'
|
|
56
|
+
/** The tool that produced the activity. */
|
|
57
|
+
tool: string
|
|
58
|
+
/**
|
|
59
|
+
* The affected target: a source file path (relative to project root) for
|
|
60
|
+
* read/fix/rollback/diff, else a summary string (e.g. the review operation).
|
|
61
|
+
*/
|
|
62
|
+
target: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** File path of the live NDJSON feed for a project root. */
|
|
66
|
+
export function liveFilePath(projectRoot: string): string {
|
|
67
|
+
return join(projectRoot, '.iterate', 'transcript-live.ndjson')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve the project root a tool execution belongs to, if any. */
|
|
71
|
+
function projectRootOf(exec: ToolExecution): string | null {
|
|
72
|
+
const cwd = exec.agent?.session?.header?.cwd
|
|
73
|
+
if (!cwd) return null
|
|
74
|
+
const resolved = resolveProjectRoot(undefined, cwd)
|
|
75
|
+
return resolved.ok ? resolved.root : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Classify a settled tool call into a live activity entry, or null to skip. */
|
|
79
|
+
export function classifyTool(
|
|
80
|
+
name: string,
|
|
81
|
+
args: unknown,
|
|
82
|
+
projectRoot: string,
|
|
83
|
+
): LiveActivityEntry | null {
|
|
84
|
+
// `read_file` is the dsh-native file reader reviewers use to inspect code.
|
|
85
|
+
if (name === 'read_file') {
|
|
86
|
+
const file =
|
|
87
|
+
args && typeof args === 'object' && typeof (args as Record<string, unknown>).path === 'string'
|
|
88
|
+
? (args as Record<string, unknown>).path as string
|
|
89
|
+
: ''
|
|
90
|
+
return file ? { ts: new Date().toISOString(), type: 'read', tool: name, target: file } : null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The iterate plugin's own tools — surface what the workflow is doing live.
|
|
94
|
+
const records: Record<string, LiveActivityEntry['type']> = {
|
|
95
|
+
iterate_fix: 'fix',
|
|
96
|
+
iterate_rollback: 'rollback',
|
|
97
|
+
iterate_diff: 'diff',
|
|
98
|
+
iterate_review: 'review',
|
|
99
|
+
iterate_triage: 'triage',
|
|
100
|
+
iterate_checkpoint: 'checkpoint',
|
|
101
|
+
iterate_validate: 'validate',
|
|
102
|
+
iterate_decision_log: 'log',
|
|
103
|
+
iterate_history: 'info',
|
|
104
|
+
iterate_prune: 'prune',
|
|
105
|
+
iterate_transcript: 'log',
|
|
106
|
+
iterate_status: 'info',
|
|
107
|
+
iterate_config: 'info',
|
|
108
|
+
iterate_context: 'info',
|
|
109
|
+
}
|
|
110
|
+
const type = records[name]
|
|
111
|
+
if (!type) return null
|
|
112
|
+
|
|
113
|
+
let target = ''
|
|
114
|
+
if (args && typeof args === 'object') {
|
|
115
|
+
const a = args as Record<string, unknown>
|
|
116
|
+
if (typeof a.file === 'string' && a.file) target = a.file
|
|
117
|
+
else if (typeof a.path === 'string' && a.path) target = a.path
|
|
118
|
+
else if (typeof a.operation === 'string' && a.operation) target = a.operation
|
|
119
|
+
else if (name === 'iterate_rollback' && typeof a.id === 'string' && a.id) {
|
|
120
|
+
target = `fix ${a.id}`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!target) target = name
|
|
124
|
+
return { ts: new Date().toISOString(), type, tool: name, target }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Append one activity record to the project's live feed (byte-capped). */
|
|
128
|
+
export async function appendLive(projectRoot: string, entry: LiveActivityEntry): Promise<void> {
|
|
129
|
+
const file = liveFilePath(projectRoot)
|
|
130
|
+
const line = JSON.stringify(entry) + '\n'
|
|
131
|
+
await mkdir(join(projectRoot, '.iterate'), { recursive: true })
|
|
132
|
+
// Amortized O(1): only read+rewrite when the file has grown past the cap.
|
|
133
|
+
try {
|
|
134
|
+
const st = await stat(file).catch(() => null)
|
|
135
|
+
if (st && st.size > LIVE_MAX_BYTES) {
|
|
136
|
+
const raw = await readFile(file, 'utf-8')
|
|
137
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
138
|
+
const tail = lines.slice(-LIVE_MAX_ENTRIES)
|
|
139
|
+
const tmp = `${file}.trim.tmp`
|
|
140
|
+
await writeFile(tmp, tail.join('\n') + '\n', 'utf-8')
|
|
141
|
+
await rename(tmp, file)
|
|
142
|
+
}
|
|
143
|
+
await appendFile(file, line, 'utf-8')
|
|
144
|
+
} catch {
|
|
145
|
+
// Fire-and-forget: never let live capture break a tool call.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Read the live feed (newest first), capped at the last LIVE_MAX_ENTRIES. */
|
|
150
|
+
export async function readLive(projectRoot: string): Promise<LiveActivityEntry[]> {
|
|
151
|
+
const file = liveFilePath(projectRoot)
|
|
152
|
+
if (!existsSync(file)) return []
|
|
153
|
+
try {
|
|
154
|
+
const raw = await readFile(file, 'utf-8')
|
|
155
|
+
const entries: LiveActivityEntry[] = []
|
|
156
|
+
for (const line of raw.split('\n')) {
|
|
157
|
+
if (!line.trim()) continue
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(line) as LiveActivityEntry
|
|
160
|
+
if (parsed && typeof parsed.ts === 'string' && typeof parsed.type === 'string') {
|
|
161
|
+
entries.push(parsed)
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// skip malformed lines
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return entries.slice(-LIVE_MAX_ENTRIES).reverse()
|
|
168
|
+
} catch {
|
|
169
|
+
return []
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Register a `tools/result` observer that captures reviewer activity into the
|
|
175
|
+
* project's live feed. Fire-and-forget; failures are swallowed.
|
|
176
|
+
*/
|
|
177
|
+
export function registerLiveCapture(ctx: Context): void {
|
|
178
|
+
ctx.on('tools/result', (exec: ToolExecution) => {
|
|
179
|
+
const root = projectRootOf(exec)
|
|
180
|
+
if (!root) return
|
|
181
|
+
const entry = classifyTool(exec.name, exec.arguments, root)
|
|
182
|
+
if (!entry) return
|
|
183
|
+
void appendLive(root, entry)
|
|
184
|
+
})
|
|
185
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -36,3 +36,8 @@ export function fixBackupPath(projectRoot: string, id: string, timestamp: string
|
|
|
36
36
|
export function checkpointPath(projectRoot: string): string {
|
|
37
37
|
return join(iterateDir(projectRoot), 'checkpoint.json')
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** Runtime-observatory transcript file (JSON). */
|
|
41
|
+
export function transcriptPath(projectRoot: string): string {
|
|
42
|
+
return join(iterateDir(projectRoot), 'transcript.json')
|
|
43
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/session-hooks.ts — dsh pipeline hooks for the iterate observatory (F8).
|
|
3
|
+
*
|
|
4
|
+
* Wires the {@link decideApproval} policy gate to dsh's `tools/pre-execute`
|
|
5
|
+
* waterfall. This is the AUTHORITATIVE approval seam for destructive iterate
|
|
6
|
+
* tools (`iterate_fix` / `iterate_rollback` / `iterate_prune` with dryRun:false):
|
|
7
|
+
*
|
|
8
|
+
* - `allow` policy → the call runs.
|
|
9
|
+
* - `deny` policy → the call is refused (fail-closed), surfaced as an
|
|
10
|
+
* error to the model.
|
|
11
|
+
* - `ask` policy → return `{ kind: 'ask', reason }`; dsh's own
|
|
12
|
+
* scheduler routes it through the `approval` service
|
|
13
|
+
* (see `@deepseek-ai/dsh-user-approval`), which
|
|
14
|
+
* prompts the human and audits an approve/deny pair
|
|
15
|
+
* on the session.
|
|
16
|
+
*
|
|
17
|
+
* We deliberately do NOT also add `approved` flags inside the tool bodies:
|
|
18
|
+
* the pre-execute waterfall consumes the human decision before the tool runs,
|
|
19
|
+
* so a second tool-internal gate would double-ask. This one gate is enough and
|
|
20
|
+
* stays dsh-native.
|
|
21
|
+
*
|
|
22
|
+
* Safety properties:
|
|
23
|
+
* - Read-only tools and non-iterate tools are always allowed (the gate only
|
|
24
|
+
* inspects the three destructive iterate toolnames).
|
|
25
|
+
* - If the project root / observatory config cannot be resolved, the policy
|
|
26
|
+
* degrades to `ask` (fail-safe: destructive writes always require consent).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { loadEffectiveConfig, resolveProjectRoot } from './config-loader.ts'
|
|
30
|
+
import { decideApproval, isDestructiveIterateTool } from './approval-gate.ts'
|
|
31
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
32
|
+
import type { ToolExecution, PreToolDecision } from '@deepseek-ai/dsh-tools'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the per-call approval decision for a tool execution.
|
|
36
|
+
* Returns a dsh `PreToolDecision` so the caller can short-circuit the caller.
|
|
37
|
+
*/
|
|
38
|
+
export function gateDecision(exec: ToolExecution): PreToolDecision {
|
|
39
|
+
// Importing the decision, and only inspecting our own tools, keeps unrelated
|
|
40
|
+
// tooling untouched. Anything we cannot classify is allowed by default.
|
|
41
|
+
if (!isDestructiveIterateTool(exec.name)) return { kind: 'allow' }
|
|
42
|
+
|
|
43
|
+
// Resolve the project root (use the call's own `path` arg, else the agent's
|
|
44
|
+
// session cwd) to read the effective observatory policy.
|
|
45
|
+
const argPath = typeof exec.arguments === 'object' && exec.arguments && !Array.isArray(exec.arguments)
|
|
46
|
+
&& typeof (exec.arguments as Record<string, unknown>).path === 'string'
|
|
47
|
+
? (exec.arguments as Record<string, unknown>).path as string
|
|
48
|
+
: undefined
|
|
49
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
50
|
+
const resolved = resolveProjectRoot(argPath, sessionCwd)
|
|
51
|
+
let policy: 'ask' | 'deny' | 'allow' = 'ask'
|
|
52
|
+
if (resolved.ok) {
|
|
53
|
+
const { config } = loadEffectiveConfig(resolved.root)
|
|
54
|
+
const p = config.observatory?.approval
|
|
55
|
+
if (p === 'deny') policy = 'deny'
|
|
56
|
+
else if (p === 'allow') policy = 'allow'
|
|
57
|
+
// anything else (including a corrupt/missing `ask`) → 'ask'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const decision = decideApproval(exec, policy)
|
|
61
|
+
if (decision.kind === 'deny') return { kind: 'deny', reason: decision.reason }
|
|
62
|
+
if (decision.kind === 'ask') return { kind: 'ask', reason: decision.reason }
|
|
63
|
+
return { kind: 'allow' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Register the `tools/pre-execute` waterfall listener that applies the
|
|
68
|
+
* observatory approval gate to every destructive iterate tool call.
|
|
69
|
+
*/
|
|
70
|
+
export function registerSessionHooks(ctx: Context): void {
|
|
71
|
+
ctx.on('tools/pre-execute', (exec: ToolExecution, next: () => Promise<PreToolDecision>) => {
|
|
72
|
+
// Never let a throwing gate break the pipeline — degrade to allow.
|
|
73
|
+
let decision: PreToolDecision
|
|
74
|
+
try {
|
|
75
|
+
decision = gateDecision(exec)
|
|
76
|
+
} catch {
|
|
77
|
+
return next()
|
|
78
|
+
}
|
|
79
|
+
if (decision.kind === 'ask') {
|
|
80
|
+
// Delegate the actual human-consent prompt + audit to dsh's approval
|
|
81
|
+
// service via the scheduler's `ask` path. `next()` here would short-circuit
|
|
82
|
+
// to allow, which would bypass consent — so return our ask decision.
|
|
83
|
+
return Promise.resolve(decision)
|
|
84
|
+
}
|
|
85
|
+
if (decision.kind === 'deny') {
|
|
86
|
+
return Promise.resolve(decision)
|
|
87
|
+
}
|
|
88
|
+
return next()
|
|
89
|
+
})
|
|
90
|
+
}
|
package/src/skill-prompt.ts
CHANGED
|
@@ -23,6 +23,7 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
23
23
|
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence, and whether the run was interrupted (a checkpoint left on disk means the previous run was interrupted and can be resumed)
|
|
24
24
|
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
25
25
|
- \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
|
|
26
|
+
- \`iterate_transcript\` — runtime observatory file (\`.iterate/transcript.json\`). \`read\` fetches the persisted manifest including any steering \`nudge\` for this run's reviewers; \`capture\` (call once after the final report) persists the per-reviewer threads, convergence trend, findings, fixes, checkpoint, and timeline so the client observatory panel reflects the run; \`nudge\` sets/clears steering text the next round's reviewers read. Purely local, never touches source files.
|
|
26
27
|
|
|
27
28
|
### When to use
|
|
28
29
|
When the user asks to review or iterate on the project (e.g. "review this project", "iterate on error handling", "check the codebase for issues", "dry-run review", "反复审查"), run an iterate **workflow** by calling the \`workflow\` tool.
|
|
@@ -78,6 +79,14 @@ const knownIntentional = (plan.knownIntentional || []) // config personalizati
|
|
|
78
79
|
let known = [] // cumulative DEDUPED findings fed back to reviewers
|
|
79
80
|
const rounds = [] // raw per-round findings
|
|
80
81
|
|
|
82
|
+
phase('transcript')
|
|
83
|
+
// Read any steering nudge written (via iterate_transcript nudge) for this run's reviewers.
|
|
84
|
+
const transRead = await agent(
|
|
85
|
+
'Call iterate_transcript({operation:"read"}) and return {nudge:<transcript.nudge ? transcript.nudge.text : null>}.',
|
|
86
|
+
Object.assign({ label: 'transcript:read' }, backend)
|
|
87
|
+
)
|
|
88
|
+
const steering = transRead && typeof transRead.nudge === 'string' && transRead.nudge ? transRead.nudge : null
|
|
89
|
+
|
|
81
90
|
phase('review')
|
|
82
91
|
for (let r = 1; r <= maxRounds; r++) {
|
|
83
92
|
log('round ' + r + ' of ' + maxRounds + ' — finding NEW issues only')
|
|
@@ -99,6 +108,7 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
99
108
|
? meta.reviewerPrompt
|
|
100
109
|
: 'Review dimension "' + dim + '".'
|
|
101
110
|
const extra =
|
|
111
|
+
(steering ? '\\n STEERING — read this first: ' + steering : '') +
|
|
102
112
|
(attachments.length > 0 ? '\\n User-attached images are part of the evidence; use their descriptions when judging (you see the metadata/descriptions below, not the pixels): ' + JSON.stringify(attachments) + '.' : '') +
|
|
103
113
|
'\\n Already-known findings (do NOT re-report): ' +
|
|
104
114
|
JSON.stringify(known) + nudge + '\\nReturn the findings JSON object.'
|
|
@@ -157,6 +167,13 @@ const metaRes = await agent(
|
|
|
157
167
|
const finalReport = metaRes && metaRes.finalReport ? metaRes.finalReport : null
|
|
158
168
|
const metaAudit = finalReport && finalReport.metaReview ? finalReport.metaReview : null
|
|
159
169
|
|
|
170
|
+
// Persist the run's observatory transcript (reviewer threads, trend, findings)
|
|
171
|
+
// so the client observatory panel reflects this review. Writes ONLY .iterate/transcript.json.
|
|
172
|
+
await agent(
|
|
173
|
+
'Call iterate_transcript({operation:"capture", mode:"dry-run", goal:' + JSON.stringify(report.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + report.convergence.totalRounds + ', findingsByRound:' + JSON.stringify(report.convergence.findingsByRound || []) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
174
|
+
Object.assign({ label: 'transcript:capture' }, backend)
|
|
175
|
+
)
|
|
176
|
+
|
|
160
177
|
return {
|
|
161
178
|
mode: 'dry-run',
|
|
162
179
|
goal: report.goal,
|
|
@@ -181,7 +198,7 @@ Key rules for dry-run:
|
|
|
181
198
|
- Stop when a round reports 0 new findings (converged) or maxReviewRounds is reached.
|
|
182
199
|
- The report (with per-round convergence stats + suggested fix priorities) is the deliverable.
|
|
183
200
|
- **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The meta-review ALSO runs the hard code-evidence gate (default on): every finding's file/line is validated against real files on disk, so any fabricated location surfaces as a critical \`EVIDENCE_VIOLATION\` and flips the verdict to \`needs_revision\`. The \`finalReport.verdict\` is \`approved\` only when the report passes every check AND every finding anchors to real, read code; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
|
|
184
|
-
- Only a single \`report\` entry may be appended to the decision log; nothing else is written.
|
|
201
|
+
- Only a single \`report\` entry may be appended to the decision log; nothing else is written to source files. The final \`iterate_transcript capture\` writes ONLY the observatory file (\`.iterate/transcript.json\`) so the client panel reflects the run — it is not a source-code write.
|
|
185
202
|
|
|
186
203
|
### Normal-mode workflow (autonomous closed loop)
|
|
187
204
|
Set \`args.mode = "normal"\`. Loop: resume → plan → parallel review ×N → atomic fixes via \`iterate_fix\` → validate → rollback on failure → checkpoint → loop → auto-stop when zero findings remain.
|
|
@@ -239,6 +256,14 @@ let fixedCount = (checkpoint && typeof checkpoint.fixedCount === 'number') ? che
|
|
|
239
256
|
let converged = false
|
|
240
257
|
let abortedByValidation = false
|
|
241
258
|
let failedCommands = []
|
|
259
|
+
const fixRecords = [] // observatory fix records collected round by round
|
|
260
|
+
|
|
261
|
+
// Read any steering nudge intended for this run's reviewers.
|
|
262
|
+
const transRead = await agent(
|
|
263
|
+
'Call iterate_transcript({operation:"read"}) and return {nudge:<transcript.nudge ? transcript.nudge.text : null>}.',
|
|
264
|
+
Object.assign({ label: 'transcript:read' }, backend)
|
|
265
|
+
)
|
|
266
|
+
const steering = transRead && typeof transRead.nudge === 'string' && transRead.nudge ? transRead.nudge : null
|
|
242
267
|
|
|
243
268
|
phase('loop')
|
|
244
269
|
for (let r = startRound; r <= maxRounds; r++) {
|
|
@@ -265,6 +290,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
265
290
|
? meta.reviewerPrompt
|
|
266
291
|
: 'Review dimension "' + dim + '" on the CURRENT code state (previous atomic findings are fixed).'
|
|
267
292
|
const extra =
|
|
293
|
+
(steering ? '\\n STEERING — read this first: ' + steering : '') +
|
|
268
294
|
(attachments.length > 0 ? '\\n User-attached images are part of the evidence; use their descriptions when judging (you see the metadata/descriptions below, not the pixels): ' + JSON.stringify(attachments) + '.' : '') +
|
|
269
295
|
'\\n Do NOT re-report already-known architectural findings: ' + JSON.stringify(architectural) + nudge + '\\nReturn the findings JSON object.'
|
|
270
296
|
return agent(base + extra, Object.assign({ label: 'review:' + dim + ':r' + r, schema: meta.findingsSchema }, backend))
|
|
@@ -309,12 +335,14 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
309
335
|
'Apply the fixes for ' + file + ' using iterate_fix. For EACH finding in this list, ' +
|
|
310
336
|
'read the current file, compute the edited full content (change <= ' + atomicMaxLines + ' lines), and call ' +
|
|
311
337
|
'iterate_fix({ file: "' + file + '", content: <full new file content>, finding: <that finding>, round: ' + r + ' }). ' +
|
|
312
|
-
'Apply the findings IN ORDER. After all fixes, call iterate_diff({ file: "' + file + '" }) to verify the accumulated diff
|
|
313
|
-
'
|
|
338
|
+
'Apply the findings IN ORDER. After all fixes, call iterate_diff({ file: "' + file + '" }) to verify the accumulated diff and ' +
|
|
339
|
+
'read its line statistics (lines added/removed). ' +
|
|
340
|
+
'Findings: ' + JSON.stringify(byFile[file]) + '. Return the array of {id, ok, error, file, linesAdded, linesRemoved} per iterate_fix call ' +
|
|
341
|
+
'(id/ok required; put the file-wide line stats from iterate_diff on each record, or on the last record and 0 elsewhere).',
|
|
314
342
|
Object.assign({ label: 'fix:' + file, phase: 'fix', schema: {
|
|
315
343
|
type: 'object', additionalProperties: false,
|
|
316
344
|
properties: {
|
|
317
|
-
fixes: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
345
|
+
fixes: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' }, file: { type: 'string' }, linesAdded: { type: 'integer' }, linesRemoved: { type: 'integer' } }, required: ['id', 'ok'] } }
|
|
318
346
|
},
|
|
319
347
|
required: ['fixes'] } }, backend)
|
|
320
348
|
)))
|
|
@@ -322,6 +350,17 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
322
350
|
if (res && Array.isArray(res.fixes)) {
|
|
323
351
|
for (const fx of res.fixes) {
|
|
324
352
|
if (fx && fx.ok === true) { fixedCount += 1; roundFixIds.push(fx.id) }
|
|
353
|
+
// Collect fix records for the observatory transcript (defensive defaults).
|
|
354
|
+
const fixFileKeys = Object.keys(byFile)
|
|
355
|
+
fixRecords.push({
|
|
356
|
+
id: fx && typeof fx.id === 'string' ? fx.id : '',
|
|
357
|
+
file: fx && typeof fx.file === 'string' ? fx.file : (fixFileKeys.length === 1 ? fixFileKeys[0] : ''),
|
|
358
|
+
round: r,
|
|
359
|
+
summary: '',
|
|
360
|
+
linesAdded: fx && typeof fx.linesAdded === 'number' ? fx.linesAdded : 0,
|
|
361
|
+
linesRemoved: fx && typeof fx.linesRemoved === 'number' ? fx.linesRemoved : 0,
|
|
362
|
+
success: !!(fx && fx.ok === true),
|
|
363
|
+
})
|
|
325
364
|
}
|
|
326
365
|
}
|
|
327
366
|
}
|
|
@@ -404,6 +443,19 @@ if (!abortedByValidation) {
|
|
|
404
443
|
{ label: 'checkpoint:clear' }
|
|
405
444
|
)
|
|
406
445
|
}
|
|
446
|
+
// Persist the run's observatory transcript (threads, trend, fixes, checkpoint)
|
|
447
|
+
// so the client observatory panel reflects the run. Writes ONLY .iterate/transcript.json.
|
|
448
|
+
const obsCheckpoint = abortedByValidation ? null : {
|
|
449
|
+
mode: 'normal',
|
|
450
|
+
round: rounds.length,
|
|
451
|
+
maxRounds: maxRounds,
|
|
452
|
+
fixedCount: fixedCount,
|
|
453
|
+
resumeCount: effectiveResumeCount,
|
|
454
|
+
}
|
|
455
|
+
await agent(
|
|
456
|
+
'Call iterate_transcript({operation:"capture", mode:"normal", goal:' + JSON.stringify(plan.goal) + ', maxRounds:' + maxRounds + ', roundsExecuted:' + rounds.length + ', findingsByRound:' + JSON.stringify(rounds.map(rr => (rr.findings && rr.findings.length) ? rr.findings.length : 0)) + ', fixes:' + JSON.stringify(fixRecords) + ', checkpoint:' + JSON.stringify(obsCheckpoint) + ', rounds:' + JSON.stringify(rounds.map(rr => ({ round: rr.round, findings: rr.findings, readFiles: rr.readFiles }))) + '}). Return {operation:"ok"}.',
|
|
457
|
+
{ label: 'transcript:capture' }
|
|
458
|
+
)
|
|
407
459
|
return {
|
|
408
460
|
mode: 'normal',
|
|
409
461
|
goal: plan.goal,
|