iterate-plugin 2.3.7 → 2.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/lib/client.js +852 -0
- package/lib/parse.js +790 -0
- package/package.json +11 -4
- package/src/config-write.ts +181 -0
- package/src/index.ts +13 -4
- package/src/paths.ts +38 -0
- package/src/review.ts +13 -6
- 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 +14 -4
- package/src/tools/fix.ts +565 -0
- package/src/tools/triage.ts +370 -0
- package/src/types.ts +64 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/checkpoint.ts — iteration checkpoint + status tools.
|
|
3
|
+
*
|
|
4
|
+
* iterate_checkpoint — save / load / clear a resume checkpoint so a long
|
|
5
|
+
* iteration can continue where it left off.
|
|
6
|
+
* iterate_status — summarize the current iteration state from the
|
|
7
|
+
* decision log, fix registry, and checkpoint.
|
|
8
|
+
*
|
|
9
|
+
* Checkpoint layout: `.iterate/checkpoint.json`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
|
+
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
15
|
+
import { resolveProjectRoot } from '../config-loader.ts'
|
|
16
|
+
import { checkpointPath, iterateDir } from '../paths.ts'
|
|
17
|
+
import { readRegistry } from './fix.ts'
|
|
18
|
+
import { readDecisionEntries } from './decision-log.ts'
|
|
19
|
+
import type { IterationCheckpoint, IterationStatus } from '../types.ts'
|
|
20
|
+
|
|
21
|
+
// ─── Pure helpers (exported for unit tests) ─────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Read a checkpoint from disk (missing/corrupt → null). */
|
|
24
|
+
export function readCheckpoint(projectRoot: string): IterationCheckpoint | null {
|
|
25
|
+
const file = checkpointPath(projectRoot)
|
|
26
|
+
if (!existsSync(file)) return null
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8')) as IterationCheckpoint
|
|
29
|
+
if (!parsed || typeof parsed !== 'object') return null
|
|
30
|
+
if (parsed.mode !== 'dry-run' && parsed.mode !== 'normal') return null
|
|
31
|
+
if (typeof parsed.round !== 'number') return null
|
|
32
|
+
return parsed
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Validate a checkpoint payload (returns error string or null). */
|
|
39
|
+
export function validateCheckpoint(input: {
|
|
40
|
+
mode: unknown
|
|
41
|
+
round: unknown
|
|
42
|
+
maxRounds: unknown
|
|
43
|
+
fixedCount: unknown
|
|
44
|
+
architecturalCount: unknown
|
|
45
|
+
}): string | null {
|
|
46
|
+
if (input.mode !== 'dry-run' && input.mode !== 'normal') {
|
|
47
|
+
return 'mode must be "dry-run" or "normal"'
|
|
48
|
+
}
|
|
49
|
+
if (typeof input.round !== 'number' || !Number.isInteger(input.round) || input.round < 0) {
|
|
50
|
+
return 'round must be a non-negative integer'
|
|
51
|
+
}
|
|
52
|
+
if (typeof input.maxRounds !== 'number' || !Number.isInteger(input.maxRounds) || input.maxRounds < 1) {
|
|
53
|
+
return 'maxRounds must be a positive integer'
|
|
54
|
+
}
|
|
55
|
+
if (typeof input.fixedCount !== 'number' || !Number.isInteger(input.fixedCount) || input.fixedCount < 0) {
|
|
56
|
+
return 'fixedCount must be a non-negative integer'
|
|
57
|
+
}
|
|
58
|
+
if (typeof input.architecturalCount !== 'number' || !Number.isInteger(input.architecturalCount) || input.architecturalCount < 0) {
|
|
59
|
+
return 'architecturalCount must be a non-negative integer'
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compute a status summary from the runtime artifacts.
|
|
66
|
+
* Pure (no I/O) — all reads are injected, so it is unit-testable.
|
|
67
|
+
*/
|
|
68
|
+
export function computeStatus(input: {
|
|
69
|
+
checkpoint: IterationCheckpoint | null
|
|
70
|
+
decisionEntries: { timestamp: string; type: string; round?: number; data?: Record<string, unknown> }[]
|
|
71
|
+
fixRegistry: { rounds: { round: number; fixedCount: number; failedCount: number }[] }
|
|
72
|
+
}): IterationStatus {
|
|
73
|
+
const checkpoint = input.checkpoint
|
|
74
|
+
const entries = input.decisionEntries
|
|
75
|
+
const registry = input.fixRegistry
|
|
76
|
+
|
|
77
|
+
const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null
|
|
78
|
+
const lastUpdated = lastEntry?.timestamp ?? checkpoint?.updatedAt ?? null
|
|
79
|
+
|
|
80
|
+
// Round = checkpoint.round (explicit) or max round seen in the decision log.
|
|
81
|
+
let currentRound = checkpoint?.round ?? 0
|
|
82
|
+
if (!checkpoint) {
|
|
83
|
+
for (const e of entries) {
|
|
84
|
+
if (typeof e.round === 'number' && e.round > currentRound) currentRound = e.round
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const totalRounds = checkpoint?.maxRounds ?? currentRound
|
|
89
|
+
const registryFixed = registry.rounds.reduce((sum, r) => sum + r.fixedCount, 0)
|
|
90
|
+
const failedCount = registry.rounds.reduce((sum, r) => sum + r.failedCount, 0)
|
|
91
|
+
// When a checkpoint exists, its snapshot fields are authoritative for resume
|
|
92
|
+
// (fixedCount / architecturalCount / findings); otherwise derive from the
|
|
93
|
+
// live fix registry and decision log.
|
|
94
|
+
const fixedCount = checkpoint ? checkpoint.fixedCount : registryFixed
|
|
95
|
+
const architecturalCount = checkpoint?.architecturalCount ?? 0
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
mode: checkpoint?.mode ?? null,
|
|
99
|
+
currentRound,
|
|
100
|
+
totalRounds,
|
|
101
|
+
fixedCount,
|
|
102
|
+
architecturalCount,
|
|
103
|
+
findingsCount: checkpoint?.findings.length ?? 0,
|
|
104
|
+
totalDecisionLogEntries: entries.length,
|
|
105
|
+
hasCheckpoint: checkpoint !== null,
|
|
106
|
+
checkpoint,
|
|
107
|
+
lastUpdated,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── iterate_checkpoint ──────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Register the `iterate_checkpoint` tool.
|
|
115
|
+
* Saves progress so the orchestrator can resume a long iteration.
|
|
116
|
+
*/
|
|
117
|
+
export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
118
|
+
ctx.tools.register(
|
|
119
|
+
defineTool({
|
|
120
|
+
name: 'iterate_checkpoint',
|
|
121
|
+
description:
|
|
122
|
+
'Save / load / clear the iteration checkpoint. The workflow saves a checkpoint at the start of ' +
|
|
123
|
+
'each round (so a long run can resume) and clears it when the iteration completes.',
|
|
124
|
+
parameters: {
|
|
125
|
+
operation: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
required: true,
|
|
128
|
+
description: '"save" to persist the current progress, "load" to read it back, "clear" to remove it.',
|
|
129
|
+
enum: ['save', 'load', 'clear'],
|
|
130
|
+
},
|
|
131
|
+
mode: { type: 'string', description: 'Required for save: "dry-run" or "normal".' },
|
|
132
|
+
round: { type: 'integer', description: 'Required for save: current round number (0 = none started).' },
|
|
133
|
+
maxRounds: { type: 'integer', description: 'Required for save: total round cap.' },
|
|
134
|
+
fixedCount: { type: 'integer', description: 'Required for save: number of fixes applied so far.' },
|
|
135
|
+
architecturalCount: { type: 'integer', description: 'Required for save: architectural findings left unfixed.' },
|
|
136
|
+
findings: { type: 'json', description: 'Optional for save: the current deduped findings to resume from.' },
|
|
137
|
+
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
output: {
|
|
141
|
+
schema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
properties: {
|
|
145
|
+
operation: { type: 'string', required: true },
|
|
146
|
+
ok: { type: 'boolean', required: true },
|
|
147
|
+
checkpoint: { type: 'json' },
|
|
148
|
+
existed: { type: 'boolean' },
|
|
149
|
+
error: { type: 'string' },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
render: (_args, value) => [
|
|
153
|
+
{ type: 'text', text: JSON.stringify(value, null, 2) },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async execute(args) {
|
|
158
|
+
const resolved = resolveProjectRoot(args.path)
|
|
159
|
+
if (!resolved.ok) return { operation: args.operation, ok: false, error: resolved.reason }
|
|
160
|
+
const projectRoot = resolved.root
|
|
161
|
+
|
|
162
|
+
if (args.operation === 'load') {
|
|
163
|
+
const checkpoint = readCheckpoint(projectRoot)
|
|
164
|
+
return { operation: 'load', ok: true, checkpoint: (checkpoint as unknown as JsonValue | null) ?? undefined }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (args.operation === 'clear') {
|
|
168
|
+
const existed = existsSync(checkpointPath(projectRoot))
|
|
169
|
+
if (existed) {
|
|
170
|
+
try { rmSync(checkpointPath(projectRoot), { force: true }) } catch (err) {
|
|
171
|
+
return { operation: 'clear', ok: false, existed, error: `failed to clear checkpoint: ${String(err)}` }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { operation: 'clear', ok: true, existed }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (args.operation === 'save') {
|
|
178
|
+
const invalid = validateCheckpoint({
|
|
179
|
+
mode: args.mode,
|
|
180
|
+
round: args.round,
|
|
181
|
+
maxRounds: args.maxRounds,
|
|
182
|
+
fixedCount: args.fixedCount,
|
|
183
|
+
architecturalCount: args.architecturalCount,
|
|
184
|
+
})
|
|
185
|
+
if (invalid) return { operation: 'save', ok: false, error: invalid }
|
|
186
|
+
const checkpoint: IterationCheckpoint = {
|
|
187
|
+
mode: args.mode as 'dry-run' | 'normal',
|
|
188
|
+
round: args.round as number,
|
|
189
|
+
maxRounds: args.maxRounds as number,
|
|
190
|
+
fixedCount: args.fixedCount as number,
|
|
191
|
+
architecturalCount: args.architecturalCount as number,
|
|
192
|
+
findings: (Array.isArray(args.findings) ? args.findings : []) as unknown as IterationCheckpoint['findings'],
|
|
193
|
+
startedAt: readCheckpoint(projectRoot)?.startedAt ?? new Date().toISOString(),
|
|
194
|
+
updatedAt: new Date().toISOString(),
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
mkdirSync(iterateDir(projectRoot), { recursive: true })
|
|
198
|
+
writeFileSync(checkpointPath(projectRoot), JSON.stringify(checkpoint, null, 2), 'utf-8')
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return { operation: 'save', ok: false, error: `failed to write checkpoint: ${String(err)}` }
|
|
201
|
+
}
|
|
202
|
+
return { operation: 'save', ok: true, checkpoint: checkpoint as unknown as JsonValue }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { operation: args.operation, ok: false, error: 'unknown operation. Use "save", "load", or "clear".' }
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── iterate_status ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Register the `iterate_status` tool.
|
|
215
|
+
* Summarizes the current iteration state (mode, round, fixed count, findings).
|
|
216
|
+
*/
|
|
217
|
+
export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
218
|
+
ctx.tools.register(
|
|
219
|
+
defineTool({
|
|
220
|
+
name: 'iterate_status',
|
|
221
|
+
description:
|
|
222
|
+
'Summarize the current iterate run: mode, current round vs total, fixes applied, architectural ' +
|
|
223
|
+
'findings remaining, decision-log size, and whether a resume checkpoint exists.',
|
|
224
|
+
parameters: {
|
|
225
|
+
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
output: {
|
|
229
|
+
schema: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
properties: {
|
|
233
|
+
ok: { type: 'boolean', required: true },
|
|
234
|
+
mode: { type: 'string' },
|
|
235
|
+
currentRound: { type: 'integer' },
|
|
236
|
+
totalRounds: { type: 'integer' },
|
|
237
|
+
fixedCount: { type: 'integer' },
|
|
238
|
+
architecturalCount: { type: 'integer' },
|
|
239
|
+
findingsCount: { type: 'integer' },
|
|
240
|
+
totalDecisionLogEntries: { type: 'integer' },
|
|
241
|
+
hasCheckpoint: { type: 'boolean' },
|
|
242
|
+
lastUpdated: { type: 'string' },
|
|
243
|
+
error: { type: 'string' },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
render: (_args, value) => {
|
|
247
|
+
if (!value.ok) return [{ type: 'text', text: `status failed: ${value.error}` }]
|
|
248
|
+
const lines = [
|
|
249
|
+
`Mode: ${value.mode ?? 'none'}`,
|
|
250
|
+
`Round: ${value.currentRound} / ${value.totalRounds}`,
|
|
251
|
+
`Fixed: ${value.fixedCount} · Architectural remaining: ${value.architecturalCount}`,
|
|
252
|
+
`Findings in checkpoint: ${value.findingsCount}`,
|
|
253
|
+
`Decision-log entries: ${value.totalDecisionLogEntries}`,
|
|
254
|
+
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}`,
|
|
255
|
+
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
256
|
+
]
|
|
257
|
+
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }]
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async execute(args) {
|
|
262
|
+
const resolved = resolveProjectRoot(args.path)
|
|
263
|
+
if (!resolved.ok) return { ok: false, error: resolved.reason }
|
|
264
|
+
const projectRoot = resolved.root
|
|
265
|
+
const status = computeStatus({
|
|
266
|
+
checkpoint: readCheckpoint(projectRoot),
|
|
267
|
+
decisionEntries: readDecisionEntries(projectRoot),
|
|
268
|
+
fixRegistry: readRegistry(projectRoot),
|
|
269
|
+
})
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
mode: status.mode ?? undefined,
|
|
273
|
+
currentRound: status.currentRound,
|
|
274
|
+
totalRounds: status.totalRounds,
|
|
275
|
+
fixedCount: status.fixedCount,
|
|
276
|
+
architecturalCount: status.architecturalCount,
|
|
277
|
+
findingsCount: status.findingsCount,
|
|
278
|
+
totalDecisionLogEntries: status.totalDecisionLogEntries,
|
|
279
|
+
hasCheckpoint: status.hasCheckpoint,
|
|
280
|
+
lastUpdated: status.lastUpdated ?? undefined,
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
}
|
package/src/tools/config.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
1
2
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
3
|
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
3
4
|
import { loadEffectiveConfig, validateConfig, resolveProjectRoot } from '../config-loader.ts'
|
|
5
|
+
import {
|
|
6
|
+
applyConfigUpdates,
|
|
7
|
+
readRawConfig,
|
|
8
|
+
validateConfigUpdates,
|
|
9
|
+
writeConfigFile,
|
|
10
|
+
} from '../config-write.ts'
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* Register the `iterate_config` tool.
|
|
7
|
-
* Reads and returns the iterate.config.yaml configuration
|
|
14
|
+
* Reads and returns the iterate.config.yaml configuration, or writes a
|
|
15
|
+
* validated partial update back to it (with backup + rollback).
|
|
8
16
|
* Model-facing: returns JSON with the full config, a specific section, or validation errors.
|
|
9
17
|
*/
|
|
10
18
|
export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
@@ -12,11 +20,17 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
12
20
|
defineTool({
|
|
13
21
|
name: 'iterate_config',
|
|
14
22
|
description:
|
|
15
|
-
'Read the iterate.config.yaml configuration from the project root. ' +
|
|
23
|
+
'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
16
24
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
|
17
|
-
'Use this to discover available dimensions, validation commands, git settings, and personalization rules
|
|
25
|
+
'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
|
|
26
|
+
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.).',
|
|
18
27
|
|
|
19
28
|
parameters: {
|
|
29
|
+
operation: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Default "read". "write" validates and applies a partial config update (backed up first).',
|
|
32
|
+
enum: ['read', 'write'],
|
|
33
|
+
},
|
|
20
34
|
path: {
|
|
21
35
|
type: 'string',
|
|
22
36
|
description: 'Project root directory (default: current working directory).',
|
|
@@ -30,6 +44,13 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
30
44
|
type: 'boolean',
|
|
31
45
|
description: 'If true, validate the config schema and return any missing fields.',
|
|
32
46
|
},
|
|
47
|
+
updates: {
|
|
48
|
+
type: 'json',
|
|
49
|
+
description:
|
|
50
|
+
'For operation "write": a partial config object to merge in, e.g. ' +
|
|
51
|
+
'{"goal":"...","dimensions":["correctness","security"],"max_rounds":5}. ' +
|
|
52
|
+
'Supported keys: goal, language, dimensions, max_rounds, review, atomic, git, validation, personalization, onboarding.',
|
|
53
|
+
},
|
|
33
54
|
},
|
|
34
55
|
|
|
35
56
|
output: {
|
|
@@ -44,6 +65,9 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
44
65
|
data: { type: 'json' },
|
|
45
66
|
config: { type: 'json' },
|
|
46
67
|
availableSections: { type: 'array', items: { type: 'string' } },
|
|
68
|
+
operation: { type: 'string' },
|
|
69
|
+
ok: { type: 'boolean' },
|
|
70
|
+
backupPath: { type: 'string' },
|
|
47
71
|
error: { type: 'string' },
|
|
48
72
|
},
|
|
49
73
|
},
|
|
@@ -58,9 +82,43 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
58
82
|
return { found: false, error: resolved.reason }
|
|
59
83
|
}
|
|
60
84
|
const projectRoot = resolved.root
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
85
|
+
|
|
86
|
+
// ── Write operation ────────────────────────────────────────────────
|
|
87
|
+
if (args.operation === 'write') {
|
|
88
|
+
const updates = args.updates as Record<string, unknown> | undefined
|
|
89
|
+
const updateErrors = validateConfigUpdates(updates ?? {})
|
|
90
|
+
if (updateErrors.length > 0) {
|
|
91
|
+
return { operation: 'write', ok: false, found: false, errors: updateErrors }
|
|
92
|
+
}
|
|
93
|
+
let base: Record<string, unknown>
|
|
94
|
+
try {
|
|
95
|
+
base = readRawConfig(join(projectRoot, 'iterate.config.yaml'))
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { operation: 'write', ok: false, found: false, error: `failed to read config: ${String(err)}` }
|
|
98
|
+
}
|
|
99
|
+
const merged = applyConfigUpdates(base, updates ?? {})
|
|
100
|
+
const schemaErrors = validateConfig(merged)
|
|
101
|
+
if (schemaErrors.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
operation: 'write',
|
|
104
|
+
ok: false,
|
|
105
|
+
found: false,
|
|
106
|
+
errors: schemaErrors.map((e) => `missing/required field: ${e}`),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const result = writeConfigFile(projectRoot, merged)
|
|
110
|
+
if (!result.ok) return { operation: 'write', ok: false, found: false, error: result.error }
|
|
111
|
+
const { config } = loadEffectiveConfig(projectRoot)
|
|
112
|
+
return {
|
|
113
|
+
operation: 'write',
|
|
114
|
+
ok: true,
|
|
115
|
+
found: true,
|
|
116
|
+
backupPath: result.backupPath ?? undefined,
|
|
117
|
+
config: config as unknown as JsonValue,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Read operations (original behavior) ────────────────────────────
|
|
64
122
|
const { config, source } = loadEffectiveConfig(projectRoot)
|
|
65
123
|
const hasOverride = source === 'override'
|
|
66
124
|
|
|
@@ -23,7 +23,7 @@ function logPath(projectRoot: string): string {
|
|
|
23
23
|
* Append one entry to the decision log (JSONL format).
|
|
24
24
|
* Returns the entry count after appending.
|
|
25
25
|
*/
|
|
26
|
-
function
|
|
26
|
+
export function appendDecisionEntry(projectRoot: string, entry: DecisionLogEntry): { count: number; path: string } {
|
|
27
27
|
const filePath = logPath(projectRoot)
|
|
28
28
|
const line = JSON.stringify(entry) + '\n'
|
|
29
29
|
appendFileSync(filePath, line, 'utf-8')
|
|
@@ -41,7 +41,7 @@ function appendEntry(projectRoot: string, entry: DecisionLogEntry): { count: num
|
|
|
41
41
|
/**
|
|
42
42
|
* Read all entries from the decision log.
|
|
43
43
|
*/
|
|
44
|
-
function
|
|
44
|
+
export function readDecisionEntries(projectRoot: string): DecisionLogEntry[] {
|
|
45
45
|
const filePath = join(projectRoot, LOG_DIR, LOG_FILE)
|
|
46
46
|
if (!existsSync(filePath)) return []
|
|
47
47
|
try {
|
|
@@ -82,6 +82,16 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
82
82
|
description:
|
|
83
83
|
'Entry type (required for append): round_start, review_result, atomic_fix, ' +
|
|
84
84
|
'architectural_fix, revert, validation, decision, report.',
|
|
85
|
+
enum: [
|
|
86
|
+
'round_start',
|
|
87
|
+
'review_result',
|
|
88
|
+
'atomic_fix',
|
|
89
|
+
'architectural_fix',
|
|
90
|
+
'revert',
|
|
91
|
+
'validation',
|
|
92
|
+
'decision',
|
|
93
|
+
'report',
|
|
94
|
+
],
|
|
85
95
|
},
|
|
86
96
|
round: {
|
|
87
97
|
type: 'integer',
|
|
@@ -124,7 +134,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
124
134
|
const projectRoot = resolved.root
|
|
125
135
|
|
|
126
136
|
if (args.operation === 'read') {
|
|
127
|
-
const entries =
|
|
137
|
+
const entries = readDecisionEntries(projectRoot)
|
|
128
138
|
return {
|
|
129
139
|
operation: 'read',
|
|
130
140
|
entryCount: entries.length,
|
|
@@ -148,7 +158,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
148
158
|
data: (args.data as Record<string, unknown>) ?? {},
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
const result =
|
|
161
|
+
const result = appendDecisionEntry(projectRoot, entry)
|
|
152
162
|
return {
|
|
153
163
|
operation: 'append',
|
|
154
164
|
success: true,
|