iterate-plugin 2.4.0 → 2.6.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 +73 -15
- package/lib/client.js +326 -29
- package/lib/parse.js +399 -0
- package/package.json +1 -1
- package/src/config-write.ts +181 -0
- package/src/index.ts +12 -1
- package/src/paths.ts +38 -0
- package/src/skill-prompt.ts +117 -24
- package/src/tools/checkpoint.ts +285 -0
- package/src/tools/config.ts +64 -6
- package/src/tools/decision-log.ts +4 -4
- package/src/tools/fix.ts +576 -0
- package/src/tools/history.ts +162 -0
- package/src/tools/prune.ts +313 -0
- package/src/tools/triage.ts +7 -0
- package/src/types.ts +64 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/history.ts — iteration history reader.
|
|
3
|
+
*
|
|
4
|
+
* iterate_history — read the decision-log entries (with optional filters)
|
|
5
|
+
* plus a summary of the fix registry, so the user or the
|
|
6
|
+
* orchestrator can review exactly what the run did.
|
|
7
|
+
*
|
|
8
|
+
* Complements `iterate_status` (compact summary) with the actual detail.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
12
|
+
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
13
|
+
import { resolveProjectRoot } from '../config-loader.ts'
|
|
14
|
+
import { readDecisionEntries } from './decision-log.ts'
|
|
15
|
+
import { readRegistry } from './fix.ts'
|
|
16
|
+
import type { DecisionLogEntry, FixRegistry } from '../types.ts'
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LIMIT = 50
|
|
19
|
+
const MAX_LIMIT = 200
|
|
20
|
+
|
|
21
|
+
/** Clamp a caller-supplied `limit` to a sane range. */
|
|
22
|
+
export function clampHistoryLimit(limit: number | undefined): number {
|
|
23
|
+
if (typeof limit !== 'number' || !Number.isInteger(limit) || limit <= 0) {
|
|
24
|
+
return DEFAULT_LIMIT
|
|
25
|
+
}
|
|
26
|
+
return Math.min(limit, MAX_LIMIT)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Filter + cap decision-log entries. Pure, unit-tested.
|
|
31
|
+
* Returns the newest `limit` matching entries plus the total match count
|
|
32
|
+
* (before the cap), so callers can tell when the result was truncated.
|
|
33
|
+
*/
|
|
34
|
+
export function filterDecisionEntries(
|
|
35
|
+
entries: DecisionLogEntry[],
|
|
36
|
+
opts: { type?: unknown; since?: unknown; limit?: unknown },
|
|
37
|
+
): { entries: DecisionLogEntry[]; filteredCount: number; limit: number } {
|
|
38
|
+
const type = typeof opts.type === 'string' && opts.type ? opts.type : undefined
|
|
39
|
+
const since = typeof opts.since === 'string' && opts.since ? opts.since : undefined
|
|
40
|
+
const limit = clampHistoryLimit(opts.limit as number | undefined)
|
|
41
|
+
|
|
42
|
+
const matching = (Array.isArray(entries) ? entries : []).filter((e) => {
|
|
43
|
+
if (type && e.type !== type) return false
|
|
44
|
+
if (since && e.timestamp <= since) return false
|
|
45
|
+
return true
|
|
46
|
+
})
|
|
47
|
+
return {
|
|
48
|
+
entries: matching.slice(-limit),
|
|
49
|
+
filteredCount: matching.length,
|
|
50
|
+
limit,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Per-round fix counts + totals from a fix registry. Pure, unit-tested. */
|
|
55
|
+
export function summarizeFixRegistry(registry: FixRegistry): {
|
|
56
|
+
totalFixed: number
|
|
57
|
+
totalFailed: number
|
|
58
|
+
roundCount: number
|
|
59
|
+
rounds: { round: number; fixedCount: number; failedCount: number }[]
|
|
60
|
+
} {
|
|
61
|
+
const rounds = (registry.rounds ?? []).map((r) => ({
|
|
62
|
+
round: r.round,
|
|
63
|
+
fixedCount: r.fixedCount,
|
|
64
|
+
failedCount: r.failedCount,
|
|
65
|
+
}))
|
|
66
|
+
return {
|
|
67
|
+
totalFixed: rounds.reduce((s, r) => s + r.fixedCount, 0),
|
|
68
|
+
totalFailed: rounds.reduce((s, r) => s + r.failedCount, 0),
|
|
69
|
+
roundCount: rounds.length,
|
|
70
|
+
rounds,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register the `iterate_history` tool.
|
|
76
|
+
* Reads the decision log (optionally filtered by type / since / limit) and a
|
|
77
|
+
* fix-registry summary. Read-only; never modifies the filesystem.
|
|
78
|
+
*/
|
|
79
|
+
export function registerHistoryTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
80
|
+
ctx.tools.register(
|
|
81
|
+
defineTool({
|
|
82
|
+
name: 'iterate_history',
|
|
83
|
+
description:
|
|
84
|
+
'Read the iteration history: decision-log entries (optionally filtered by entry `type`, `since` ' +
|
|
85
|
+
'timestamp, and a `limit`) plus a summary of the fix registry (per-round fixed/failed counts). ' +
|
|
86
|
+
'Read-only — use it to review what the run did, audit a log, or inspect fixes.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description:
|
|
91
|
+
'Optional entry-type filter: round_start, review_result, atomic_fix, architectural_fix, ' +
|
|
92
|
+
'revert, validation, decision, report.',
|
|
93
|
+
},
|
|
94
|
+
since: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Optional ISO timestamp; only entries AFTER this timestamp are returned.',
|
|
97
|
+
},
|
|
98
|
+
limit: {
|
|
99
|
+
type: 'integer',
|
|
100
|
+
description: `Max entries to return (default: ${DEFAULT_LIMIT}, cap: ${MAX_LIMIT}). Newest first.`,
|
|
101
|
+
},
|
|
102
|
+
path: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Project root directory (default: current working directory).',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
output: {
|
|
109
|
+
schema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
properties: {
|
|
113
|
+
ok: { type: 'boolean', required: true },
|
|
114
|
+
kind: { type: 'string' },
|
|
115
|
+
count: { type: 'integer' },
|
|
116
|
+
filteredCount: { type: 'integer' },
|
|
117
|
+
limit: { type: 'integer' },
|
|
118
|
+
log: { type: 'json' },
|
|
119
|
+
fixes: { type: 'json' },
|
|
120
|
+
error: { type: 'string' },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
render: (_args, value) => {
|
|
124
|
+
if (!value.ok) return [{ type: 'text', text: `history failed: ${value.error}` }]
|
|
125
|
+
const log = (value.log as DecisionLogEntry[] | undefined) ?? []
|
|
126
|
+
const fixes = (value.fixes as { totalFixed: number; totalFailed: number; roundCount: number } | undefined)
|
|
127
|
+
const lines = [
|
|
128
|
+
`Decision-log entries: ${value.count} (filtered to ${value.limit})`,
|
|
129
|
+
fixes
|
|
130
|
+
? `Fixes: ${fixes.totalFixed} applied · ${fixes.totalFailed} failed · across ${fixes.roundCount} round(s)`
|
|
131
|
+
: 'Fixes: none',
|
|
132
|
+
'',
|
|
133
|
+
...log.map((e) => `[${e.timestamp}] r${e.round} ${e.type}: ${JSON.stringify(e.data ?? {})}`),
|
|
134
|
+
]
|
|
135
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async execute(args) {
|
|
140
|
+
const resolved = resolveProjectRoot(args.path)
|
|
141
|
+
if (!resolved.ok) return { ok: false, kind: 'history', error: resolved.reason }
|
|
142
|
+
const projectRoot = resolved.root
|
|
143
|
+
|
|
144
|
+
const { entries, filteredCount, limit } = filterDecisionEntries(
|
|
145
|
+
readDecisionEntries(projectRoot),
|
|
146
|
+
{ type: args.type, since: args.since, limit: args.limit },
|
|
147
|
+
)
|
|
148
|
+
const fixes = summarizeFixRegistry(readRegistry(projectRoot))
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
kind: 'history',
|
|
153
|
+
count: entries.length,
|
|
154
|
+
filteredCount,
|
|
155
|
+
limit,
|
|
156
|
+
log: entries as unknown as JsonValue,
|
|
157
|
+
fixes: fixes as unknown as JsonValue,
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/prune.ts — runtime artifact cleanup for the iterate loop.
|
|
3
|
+
*
|
|
4
|
+
* iterate_prune — inspect or remove stale runtime artifacts (.iterate/).
|
|
5
|
+
* Defaults to dry-run (report-only); set `dryRun: false` to
|
|
6
|
+
* actually delete.
|
|
7
|
+
*
|
|
8
|
+
* Artifacts managed:
|
|
9
|
+
* - Decision-log entries older than `retainDays` (default 30, via since).
|
|
10
|
+
* - Stale checkpoint files (checkpoint.json).
|
|
11
|
+
* - Fix backups left over from old rounds (backups whose fix-id no longer
|
|
12
|
+
* appears in the registry).
|
|
13
|
+
* - Empty fix rounds (rounds with 0 records).
|
|
14
|
+
*
|
|
15
|
+
* Security model:
|
|
16
|
+
* - Only operates under the resolved project `.iterate/` directory.
|
|
17
|
+
* - dryRun=true by default — the caller must explicitly opt into deletion.
|
|
18
|
+
* - Each deletion is logged to the decision log (when not dry-run).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
24
|
+
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
25
|
+
import { resolveProjectRoot } from '../config-loader.ts'
|
|
26
|
+
import { readDecisionEntries, appendDecisionEntry } from './decision-log.ts'
|
|
27
|
+
import { readRegistry, removeRecord, recomputeRoundCounts } from './fix.ts'
|
|
28
|
+
import { iterateDir, fixesDir, checkpointPath, fixRegistryPath } from '../paths.ts'
|
|
29
|
+
import type { FixRegistry } from '../types.ts'
|
|
30
|
+
|
|
31
|
+
/** Default retention for decision-log entries (in days). */
|
|
32
|
+
const DEFAULT_RETAIN_DAYS = 30
|
|
33
|
+
const MIN_RETAIN_DAYS = 1
|
|
34
|
+
const MAX_RETAIN_DAYS = 365
|
|
35
|
+
|
|
36
|
+
/** Clamp retainDays to a sane range. */
|
|
37
|
+
export function clampRetainDays(days: number | undefined): number {
|
|
38
|
+
if (typeof days !== 'number' || !Number.isInteger(days) || days <= 0) {
|
|
39
|
+
return DEFAULT_RETAIN_DAYS
|
|
40
|
+
}
|
|
41
|
+
return Math.min(Math.max(days, MIN_RETAIN_DAYS), MAX_RETAIN_DAYS)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build the cutoff timestamp for a given retainDays. */
|
|
45
|
+
export function cutoffTimestamp(retainDays: number): string {
|
|
46
|
+
const d = new Date()
|
|
47
|
+
d.setDate(d.getDate() - retainDays)
|
|
48
|
+
return d.toISOString()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Inspect the runtime state and report what would be pruned.
|
|
53
|
+
* Pure (no deletions). Returns a structured report.
|
|
54
|
+
*/
|
|
55
|
+
export function inspectPrune(
|
|
56
|
+
projectRoot: string,
|
|
57
|
+
retainDays: number,
|
|
58
|
+
): {
|
|
59
|
+
oldLogEntries: number
|
|
60
|
+
hasCheckpoint: boolean
|
|
61
|
+
staleBackups: string[]
|
|
62
|
+
emptyRounds: number[]
|
|
63
|
+
totalLogEntries: number
|
|
64
|
+
registryRounds: number
|
|
65
|
+
} {
|
|
66
|
+
const cutoff = cutoffTimestamp(retainDays)
|
|
67
|
+
|
|
68
|
+
// 1. Decision-log entries older than retainDays.
|
|
69
|
+
const entries = readDecisionEntries(projectRoot)
|
|
70
|
+
const oldLogEntries = entries.filter((e) => e.timestamp < cutoff).length
|
|
71
|
+
|
|
72
|
+
// 2. Checkpoint presence.
|
|
73
|
+
const hasCheckpoint = existsSync(checkpointPath(projectRoot))
|
|
74
|
+
|
|
75
|
+
// 3. Stale fix backups: .bak files whose fix-id prefix is not in the registry.
|
|
76
|
+
const registry = readRegistry(projectRoot)
|
|
77
|
+
const activeIds = new Set<string>()
|
|
78
|
+
for (const r of registry.rounds) {
|
|
79
|
+
for (const rec of r.records) {
|
|
80
|
+
activeIds.add(rec.id)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const staleBackups: string[] = []
|
|
84
|
+
const fixDir = fixesDir(projectRoot)
|
|
85
|
+
if (existsSync(fixDir)) {
|
|
86
|
+
for (const entry of readdirSync(fixDir)) {
|
|
87
|
+
if (!entry.endsWith('.bak')) continue
|
|
88
|
+
// Extract the fix-id prefix (up to the first underscore after the id).
|
|
89
|
+
// e.g. "fix-abc123_2026-08-17T00-00-00-000Z.bak" → "fix-abc123"
|
|
90
|
+
const match = entry.match(/^(fix-[a-z0-9]+)_/)
|
|
91
|
+
const id = match?.[1]
|
|
92
|
+
if (id && !activeIds.has(id)) {
|
|
93
|
+
staleBackups.push(entry)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Empty rounds (rounds with 0 records).
|
|
99
|
+
const emptyRounds = registry.rounds
|
|
100
|
+
.filter((r) => r.records.length === 0)
|
|
101
|
+
.map((r) => r.round)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
oldLogEntries,
|
|
105
|
+
hasCheckpoint,
|
|
106
|
+
staleBackups,
|
|
107
|
+
emptyRounds,
|
|
108
|
+
totalLogEntries: entries.length,
|
|
109
|
+
registryRounds: registry.rounds.length,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Actually prune the runtime artifacts (only called when dryRun=false).
|
|
115
|
+
* Returns a detailed report of what was deleted.
|
|
116
|
+
*/
|
|
117
|
+
export function executePrune(
|
|
118
|
+
projectRoot: string,
|
|
119
|
+
retainDays: number,
|
|
120
|
+
report: ReturnType<typeof inspectPrune>,
|
|
121
|
+
): {
|
|
122
|
+
deletedLogEntries: number
|
|
123
|
+
deletedCheckpoint: boolean
|
|
124
|
+
deletedBackups: string[]
|
|
125
|
+
trimmedEmptyRounds: number
|
|
126
|
+
errors: string[]
|
|
127
|
+
} {
|
|
128
|
+
const cutoff = cutoffTimestamp(retainDays)
|
|
129
|
+
const result = {
|
|
130
|
+
deletedLogEntries: 0,
|
|
131
|
+
deletedCheckpoint: false,
|
|
132
|
+
deletedBackups: [] as string[],
|
|
133
|
+
trimmedEmptyRounds: 0,
|
|
134
|
+
errors: [] as string[],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 1. Rewrite the decision log, keeping only recent entries.
|
|
138
|
+
try {
|
|
139
|
+
const entries = readDecisionEntries(projectRoot)
|
|
140
|
+
const kept = entries.filter((e) => e.timestamp >= cutoff)
|
|
141
|
+
result.deletedLogEntries = entries.length - kept.length
|
|
142
|
+
if (result.deletedLogEntries > 0) {
|
|
143
|
+
writeFileSync(
|
|
144
|
+
join(iterateDir(projectRoot), 'decision-log.jsonl'),
|
|
145
|
+
kept.map((e) => JSON.stringify(e)).join('\n') + '\n',
|
|
146
|
+
'utf-8',
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
result.errors.push(`failed to rewrite decision log: ${String(err)}`)
|
|
151
|
+
result.deletedLogEntries = 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Remove checkpoint.
|
|
155
|
+
if (report.hasCheckpoint) {
|
|
156
|
+
try {
|
|
157
|
+
rmSync(checkpointPath(projectRoot), { force: true })
|
|
158
|
+
result.deletedCheckpoint = true
|
|
159
|
+
} catch (err) {
|
|
160
|
+
result.errors.push(`failed to remove checkpoint: ${String(err)}`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 3. Delete stale backups.
|
|
165
|
+
for (const bak of report.staleBackups) {
|
|
166
|
+
try {
|
|
167
|
+
unlinkSync(join(fixesDir(projectRoot), bak))
|
|
168
|
+
result.deletedBackups.push(bak)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
result.errors.push(`failed to delete backup ${bak}: ${String(err)}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. Trim empty rounds from the registry.
|
|
175
|
+
if (report.emptyRounds.length > 0) {
|
|
176
|
+
try {
|
|
177
|
+
let registry = readRegistry(projectRoot)
|
|
178
|
+
for (const round of report.emptyRounds) {
|
|
179
|
+
for (const rec of [...registry.rounds.find((r) => r.round === round)?.records ?? []]) {
|
|
180
|
+
registry = removeRecord(registry, rec.id)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
registry = recomputeRoundCounts(registry)
|
|
184
|
+
writeFileSync(fixRegistryPath(projectRoot), JSON.stringify(registry, null, 2), 'utf-8')
|
|
185
|
+
result.trimmedEmptyRounds = report.emptyRounds.length
|
|
186
|
+
} catch (err) {
|
|
187
|
+
result.errors.push(`failed to trim empty rounds: ${String(err)}`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Register the `iterate_prune` tool.
|
|
196
|
+
* Defaults to dry-run: inspects the runtime state and reports what would be
|
|
197
|
+
* cleaned up. Pass `dryRun: false` to actually delete.
|
|
198
|
+
*/
|
|
199
|
+
export function registerPruneTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
200
|
+
ctx.tools.register(
|
|
201
|
+
defineTool({
|
|
202
|
+
name: 'iterate_prune',
|
|
203
|
+
description:
|
|
204
|
+
'Inspect or clean up old iterate runtime artifacts (.iterate/). ' +
|
|
205
|
+
'Defaults to dry-run (report-only, no deletion). Pass `dryRun: false` to actually prune. ' +
|
|
206
|
+
'Manages: old decision-log entries, stale checkpoints, orphaned fix backups, empty fix rounds. ' +
|
|
207
|
+
'Each deletion is logged to the decision log.',
|
|
208
|
+
parameters: {
|
|
209
|
+
dryRun: {
|
|
210
|
+
type: 'boolean',
|
|
211
|
+
description: 'When true (default), only report what would be pruned without deleting anything.',
|
|
212
|
+
},
|
|
213
|
+
retainDays: {
|
|
214
|
+
type: 'integer',
|
|
215
|
+
description: `Keep entries newer than this many days (default: ${DEFAULT_RETAIN_DAYS}, range: ${MIN_RETAIN_DAYS}-${MAX_RETAIN_DAYS}).`,
|
|
216
|
+
},
|
|
217
|
+
path: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
description: 'Project root directory (default: current working directory).',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
output: {
|
|
224
|
+
schema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
properties: {
|
|
228
|
+
ok: { type: 'boolean', required: true },
|
|
229
|
+
dryRun: { type: 'boolean', required: true },
|
|
230
|
+
retainDays: { type: 'integer' },
|
|
231
|
+
report: { type: 'json' },
|
|
232
|
+
result: { type: 'json' },
|
|
233
|
+
error: { type: 'string' },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
render: (_args, value) => {
|
|
237
|
+
if (!value.ok) return [{ type: 'text', text: `prune failed: ${value.error}` }]
|
|
238
|
+
const report = value.report as Record<string, unknown> | undefined
|
|
239
|
+
const result = value.result as Record<string, unknown> | undefined
|
|
240
|
+
if (value.dryRun) {
|
|
241
|
+
const lines = [
|
|
242
|
+
`[dry-run] prune report (retainDays=${value.retainDays}):`,
|
|
243
|
+
` Decision-log entries to remove: ${report?.oldLogEntries ?? '?'} (of ${report?.totalLogEntries ?? '?'})`,
|
|
244
|
+
` Checkpoint to delete: ${report?.hasCheckpoint ? 'yes' : 'none'}`,
|
|
245
|
+
` Stale backups to delete: ${(report?.staleBackups as string[] | undefined)?.length ?? 0}`,
|
|
246
|
+
` Empty rounds to trim: ${(report?.emptyRounds as number[] | undefined)?.length ?? 0}`,
|
|
247
|
+
'',
|
|
248
|
+
'Pass dryRun:false to execute the prune.',
|
|
249
|
+
]
|
|
250
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
251
|
+
}
|
|
252
|
+
const lines = [
|
|
253
|
+
`Prune complete (retainDays=${value.retainDays}):`,
|
|
254
|
+
` Deleted ${result?.deletedLogEntries ?? 0} old log entries.`,
|
|
255
|
+
` Checkpoint deleted: ${result?.deletedCheckpoint ? 'yes' : 'no'}`,
|
|
256
|
+
` Deleted ${(result?.deletedBackups as string[] | undefined)?.length ?? 0} stale backups.`,
|
|
257
|
+
` Trimmed ${result?.trimmedEmptyRounds ?? 0} empty rounds.`,
|
|
258
|
+
]
|
|
259
|
+
const errs = (result?.errors as string[] | undefined) ?? []
|
|
260
|
+
if (errs.length > 0) {
|
|
261
|
+
lines.push('', ' Warnings:')
|
|
262
|
+
for (const e of errs) lines.push(` - ${e}`)
|
|
263
|
+
}
|
|
264
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async execute(args) {
|
|
269
|
+
const resolved = resolveProjectRoot(args.path)
|
|
270
|
+
if (!resolved.ok) return { ok: false, dryRun: true, error: resolved.reason }
|
|
271
|
+
const projectRoot = resolved.root
|
|
272
|
+
const retainDays = clampRetainDays(args.retainDays as number | undefined)
|
|
273
|
+
const dryRun = args.dryRun !== false
|
|
274
|
+
|
|
275
|
+
const report = inspectPrune(projectRoot, retainDays)
|
|
276
|
+
|
|
277
|
+
if (dryRun) {
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
dryRun: true,
|
|
281
|
+
retainDays,
|
|
282
|
+
report: report as unknown as JsonValue,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const result = executePrune(projectRoot, retainDays, report)
|
|
287
|
+
|
|
288
|
+
// Log the prune to the decision log.
|
|
289
|
+
appendDecisionEntry(projectRoot, {
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
round: 0,
|
|
292
|
+
type: 'decision',
|
|
293
|
+
data: {
|
|
294
|
+
action: 'prune',
|
|
295
|
+
retainDays,
|
|
296
|
+
deletedLogEntries: result.deletedLogEntries,
|
|
297
|
+
deletedCheckpoint: result.deletedCheckpoint,
|
|
298
|
+
deletedBackups: result.deletedBackups.length,
|
|
299
|
+
trimmedEmptyRounds: result.trimmedEmptyRounds,
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
dryRun: false,
|
|
306
|
+
retainDays,
|
|
307
|
+
report: report as unknown as JsonValue,
|
|
308
|
+
result: result as unknown as JsonValue,
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
)
|
|
313
|
+
}
|
package/src/tools/triage.ts
CHANGED
|
@@ -12,6 +12,9 @@ const CONFIG_FILE = 'iterate.config.yaml'
|
|
|
12
12
|
const PERSONALIZATION_KEY = 'personalization'
|
|
13
13
|
const KNOWN_INTENTIONAL_KEY = 'known_intentional'
|
|
14
14
|
|
|
15
|
+
/** Max entries per single `apply` call. */
|
|
16
|
+
const MAX_ENTRIES = 500
|
|
17
|
+
|
|
15
18
|
/** Whole-file marker line (matches review.ts filterKnownIntentional semantics). */
|
|
16
19
|
const WHOLE_FILE_LINE = 0
|
|
17
20
|
|
|
@@ -45,6 +48,10 @@ export function validateTriageEntries(entries: unknown): string[] {
|
|
|
45
48
|
errors.push('entries must be an array')
|
|
46
49
|
return errors
|
|
47
50
|
}
|
|
51
|
+
if (entries.length > MAX_ENTRIES) {
|
|
52
|
+
errors.push(`entries must not exceed ${MAX_ENTRIES} items (got ${entries.length})`)
|
|
53
|
+
return errors
|
|
54
|
+
}
|
|
48
55
|
for (let i = 0; i < entries.length; i++) {
|
|
49
56
|
const prefix = `entries[${i}]`
|
|
50
57
|
const e = entries[i]
|
package/src/types.ts
CHANGED
|
@@ -97,4 +97,68 @@ export interface KnownIntentional {
|
|
|
97
97
|
line?: number
|
|
98
98
|
dimension: string
|
|
99
99
|
reason: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** ─── Fix system ──────────────────────────────────────────────────────────── */
|
|
103
|
+
|
|
104
|
+
/** A single fix record: one finding → one fix operation on one file. */
|
|
105
|
+
export interface FixRecord {
|
|
106
|
+
id: string
|
|
107
|
+
timestamp: string
|
|
108
|
+
round: number
|
|
109
|
+
finding: ReviewFinding
|
|
110
|
+
backupPath: string
|
|
111
|
+
diffSummary: string
|
|
112
|
+
linesAdded: number
|
|
113
|
+
linesRemoved: number
|
|
114
|
+
success: boolean
|
|
115
|
+
error?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fix registry metadata (per-round summary). */
|
|
119
|
+
export interface FixRegistry {
|
|
120
|
+
rounds: FixRoundRecord[]
|
|
121
|
+
}
|
|
122
|
+
export interface FixRoundRecord {
|
|
123
|
+
round: number
|
|
124
|
+
fixedCount: number
|
|
125
|
+
failedCount: number
|
|
126
|
+
records: FixRecord[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Hunk-level diff for a single file. */
|
|
130
|
+
export interface FileDiffHunk {
|
|
131
|
+
oldStart: number
|
|
132
|
+
oldLines: number
|
|
133
|
+
newStart: number
|
|
134
|
+
newLines: number
|
|
135
|
+
content: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** ─── Checkpoint / resume ─────────────────────────────────────────────────── */
|
|
139
|
+
|
|
140
|
+
export interface IterationCheckpoint {
|
|
141
|
+
mode: 'dry-run' | 'normal'
|
|
142
|
+
round: number
|
|
143
|
+
maxRounds: number
|
|
144
|
+
fixedCount: number
|
|
145
|
+
architecturalCount: number
|
|
146
|
+
findings: ReviewFinding[]
|
|
147
|
+
startedAt: string
|
|
148
|
+
updatedAt: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** ─── Status summary ──────────────────────────────────────────────────────── */
|
|
152
|
+
|
|
153
|
+
export interface IterationStatus {
|
|
154
|
+
mode: 'dry-run' | 'normal' | null
|
|
155
|
+
currentRound: number
|
|
156
|
+
totalRounds: number
|
|
157
|
+
fixedCount: number
|
|
158
|
+
architecturalCount: number
|
|
159
|
+
findingsCount: number
|
|
160
|
+
totalDecisionLogEntries: number
|
|
161
|
+
hasCheckpoint: boolean
|
|
162
|
+
checkpoint: IterationCheckpoint | null
|
|
163
|
+
lastUpdated: string | null
|
|
100
164
|
}
|