iterate-plugin 3.8.0 → 3.9.1
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 +10 -7
- package/README.zh-CN.md +4 -3
- package/dist/file-lock.js +29 -15
- package/dist/git-scope.js +25 -10
- package/dist/index.js +9 -4
- package/dist/report-render.js +462 -0
- package/dist/session-record.js +268 -0
- package/dist/skill-prompt.js +2 -1
- package/dist/tools/report.js +239 -0
- package/dist/tools/transcript.js +217 -9
- package/dist/transcript.js +18 -0
- package/lib/client.js +460 -13
- package/package.json +17 -10
- package/src/client/i18n.ts +6 -0
- package/src/client/index.ts +44 -0
- package/src/file-lock.ts +47 -9
- package/src/git-scope.ts +29 -15
- package/src/index.ts +9 -4
- package/src/report-render.ts +589 -0
- package/src/session-record.ts +355 -0
- package/src/skill-prompt.ts +2 -1
- package/src/tools/report.ts +244 -0
- package/src/tools/transcript.ts +226 -9
- package/src/transcript.ts +18 -0
package/src/file-lock.ts
CHANGED
|
@@ -133,15 +133,21 @@ export function acquireProjectLockChecked(
|
|
|
133
133
|
fd = openSync(lockPath, 'wx')
|
|
134
134
|
} catch (err) {
|
|
135
135
|
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') return { acquired: false, release: () => {} }
|
|
136
|
-
// Lock exists — steal it when the owner is gone or the
|
|
136
|
+
// Lock exists — steal it when the owner is VERIFIABLY gone, or when the
|
|
137
|
+
// lock file is ancient. The stamp is read defensively: a lock is created
|
|
138
|
+
// empty (`openSync(..., 'wx')`) and the holder's pid lands a moment later,
|
|
139
|
+
// so an empty/garbled stamp means "just created, holder still writing" —
|
|
140
|
+
// treating `Number('') === 0` as a dead owner would let a contender steal
|
|
141
|
+
// a live lock and enter the same critical section. Only a parseable
|
|
142
|
+
// positive pid that is dead (or an ancient mtime) is stolen.
|
|
137
143
|
let stale = false
|
|
138
144
|
try {
|
|
139
145
|
const st = statSync(lockPath)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
const ancient = Date.now() - st.mtimeMs > staleMs
|
|
147
|
+
const raw = readFileSync(lockPath, 'utf-8').trim()
|
|
148
|
+
const pid = /^\d+$/.test(raw) ? Number(raw) : 0
|
|
149
|
+
if (pid > 0) stale = !processAlive(pid) || ancient
|
|
150
|
+
else stale = ancient
|
|
145
151
|
} catch {
|
|
146
152
|
stale = true // unreadable/vanishing lock → retry as stale
|
|
147
153
|
}
|
|
@@ -265,17 +271,49 @@ export function makeLockRelease(lockPath: string, ino?: number): () => void {
|
|
|
265
271
|
* Run `fn` while holding the advisory lock named `name`.
|
|
266
272
|
* The release is guaranteed via `finally`; `fn`'s value or error propagates
|
|
267
273
|
* unchanged. Not reentrant for the same (projectRoot, name) — see header.
|
|
274
|
+
*
|
|
275
|
+
* `fn` may return a promise: in that case the lock is held until it settles, so
|
|
276
|
+
* a read-modify-write that spans `await`ed fs calls is actually serialized
|
|
277
|
+
* (releasing when `fn()` *returns its promise* would defeat the lock entirely).
|
|
268
278
|
*/
|
|
269
279
|
export function withProjectLock<T>(
|
|
270
280
|
projectRoot: string,
|
|
271
281
|
name: string,
|
|
272
282
|
fn: () => T,
|
|
283
|
+
options?: LockOptions,
|
|
284
|
+
): T
|
|
285
|
+
export function withProjectLock<T>(
|
|
286
|
+
projectRoot: string,
|
|
287
|
+
name: string,
|
|
288
|
+
fn: () => Promise<T>,
|
|
289
|
+
options?: LockOptions,
|
|
290
|
+
): Promise<T>
|
|
291
|
+
export function withProjectLock<T>(
|
|
292
|
+
projectRoot: string,
|
|
293
|
+
name: string,
|
|
294
|
+
fn: () => T | Promise<T>,
|
|
273
295
|
options: LockOptions = {},
|
|
274
|
-
): T {
|
|
296
|
+
): T | Promise<T> {
|
|
275
297
|
const release = acquireProjectLock(projectRoot, name, options)
|
|
298
|
+
let out: T | Promise<T>
|
|
276
299
|
try {
|
|
277
|
-
|
|
278
|
-
}
|
|
300
|
+
out = fn()
|
|
301
|
+
} catch (err) {
|
|
279
302
|
release()
|
|
303
|
+
throw err
|
|
304
|
+
}
|
|
305
|
+
if (out && typeof (out as Promise<T>).then === 'function') {
|
|
306
|
+
return (out as Promise<T>).then(
|
|
307
|
+
(value) => {
|
|
308
|
+
release()
|
|
309
|
+
return value
|
|
310
|
+
},
|
|
311
|
+
(err) => {
|
|
312
|
+
release()
|
|
313
|
+
throw err
|
|
314
|
+
},
|
|
315
|
+
)
|
|
280
316
|
}
|
|
317
|
+
release()
|
|
318
|
+
return out
|
|
281
319
|
}
|
package/src/git-scope.ts
CHANGED
|
@@ -177,21 +177,35 @@ export function runGit(
|
|
|
177
177
|
opts: { signal?: AbortSignal } = {},
|
|
178
178
|
): Promise<{ ok: boolean; stdout: string; stderr: string; exitCode: number }> {
|
|
179
179
|
return new Promise((resolve) => {
|
|
180
|
-
execFile(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
180
|
+
// execFile can THROW SYNCHRONOUSLY (rather than call back with an error)
|
|
181
|
+
// when an argument cannot be encoded — e.g. a NUL or lone surrogate in the
|
|
182
|
+
// config-supplied target branch. Without this guard that throw escapes the
|
|
183
|
+
// promise executor, rejecting `runGit` and propagating out of the tool call
|
|
184
|
+
// instead of degrading to the documented full-scope fallback.
|
|
185
|
+
try {
|
|
186
|
+
execFile(
|
|
187
|
+
'git',
|
|
188
|
+
args,
|
|
189
|
+
{
|
|
190
|
+
cwd,
|
|
191
|
+
timeout: 30_000,
|
|
192
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
193
|
+
env: { ...process.env, PAGER: 'cat' },
|
|
194
|
+
signal: opts.signal,
|
|
195
|
+
},
|
|
196
|
+
(error, stdout, stderr) => {
|
|
197
|
+
const exitCode = error ? (typeof error.code === 'number' ? error.code : 1) : 0
|
|
198
|
+
resolve({ ok: exitCode === 0, stdout: stdout ?? '', stderr: stderr ?? '', exitCode })
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
} catch (err) {
|
|
202
|
+
resolve({
|
|
203
|
+
ok: false,
|
|
204
|
+
stdout: '',
|
|
205
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
206
|
+
exitCode: 1,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
195
209
|
})
|
|
196
210
|
}
|
|
197
211
|
|
package/src/index.ts
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
|
|
3
3
|
*
|
|
4
4
|
* Architecture:
|
|
5
|
-
* - The plugin registers
|
|
5
|
+
* - The plugin registers 18 tools (14 original + 3 v3.0 quality command center
|
|
6
|
+
* tools + 1 v3.9 report export)
|
|
6
7
|
* Original: config, validate, decision-log, context, review, triage, fix, diff,
|
|
7
8
|
* rollback, checkpoint, status, history, prune, transcript
|
|
8
9
|
* v3.0: experience, quality_gate, defense_events
|
|
10
|
+
* v3.9: report
|
|
9
11
|
* - The plugin injects a system prompt section teaching the iterate workflow pattern
|
|
10
12
|
* - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
|
|
11
13
|
* - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
|
|
12
|
-
* - Subagents use the
|
|
14
|
+
* - Subagents use the 18 tools to do real work (read config, run validation, log decisions,
|
|
13
15
|
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript,
|
|
14
|
-
* query experience bank, check quality gates, query defense events)
|
|
16
|
+
* query experience bank, check quality gates, query defense events, export a report)
|
|
15
17
|
* - A `tools/pre-execute` hook gates destructive iterate calls behind human approval
|
|
16
18
|
* (F8 observatory approval policy: ask / deny / allow).
|
|
17
19
|
*
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
*
|
|
24
26
|
* Key files:
|
|
25
27
|
* - src/index.ts — Plugin entry: register tools + inject skill prompt
|
|
26
|
-
* - src/tools/ —
|
|
28
|
+
* - src/tools/ — 18 tool implementations (14 original + 3 v3.0 + 1 v3.9)
|
|
27
29
|
* - src/config-loader.ts — YAML config loading
|
|
28
30
|
* - src/types.ts — Shared types
|
|
29
31
|
*/
|
|
@@ -43,6 +45,7 @@ import { registerTranscriptTool } from './tools/transcript.ts'
|
|
|
43
45
|
import { registerExperienceBankTool } from './tools/experience-bank.ts'
|
|
44
46
|
import { registerQualityGateTool } from './tools/quality-gate.ts'
|
|
45
47
|
import { registerDefenseEventsTool } from './tools/defense-events.ts'
|
|
48
|
+
import { registerReportTool } from './tools/report.ts'
|
|
46
49
|
import { registerSessionHooks } from './session-hooks.ts'
|
|
47
50
|
import { registerLiveCapture } from './live.ts'
|
|
48
51
|
import { installWorkingDirectoryContext } from './working-directory.ts'
|
|
@@ -92,6 +95,8 @@ export function apply(ctx: Context): void {
|
|
|
92
95
|
['iterate_experience', () => registerExperienceBankTool(ctx)],
|
|
93
96
|
['iterate_quality_gate', () => registerQualityGateTool(ctx)],
|
|
94
97
|
['iterate_defense_events', () => registerDefenseEventsTool(ctx)],
|
|
98
|
+
// v3.9: PR-ready / CI-ready report export
|
|
99
|
+
['iterate_report', () => registerReportTool(ctx)],
|
|
95
100
|
]
|
|
96
101
|
for (const [toolName, register] of toolRegistrations) {
|
|
97
102
|
try {
|