iterate-plugin 2.12.3 → 3.3.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 +124 -76
- package/README.zh-CN.md +129 -80
- package/dist/approval-gate.js +16 -19
- package/dist/config-loader.js +5 -0
- package/dist/index.js +16 -6
- package/dist/session-hooks.js +36 -11
- package/dist/skill-prompt.js +3 -0
- package/dist/tools/checkpoint.js +22 -3
- package/dist/tools/decision-log.js +10 -1
- package/dist/tools/defense-events.js +263 -0
- package/dist/tools/defense-store.js +103 -0
- package/dist/tools/experience-bank.js +251 -0
- package/dist/tools/experience-store.js +138 -0
- package/dist/tools/quality-gate.js +183 -0
- package/dist/tools/quality-store.js +180 -0
- package/dist/tools/transcript.js +8 -1
- package/dist/transcript.js +10 -0
- package/lib/client.js +779 -15
- package/lib/parse.js +352 -0
- package/package.json +6 -6
- package/src/approval-gate.ts +14 -28
- package/src/client/index.ts +564 -16
- package/src/config-loader.ts +5 -0
- package/src/index.ts +16 -6
- package/src/session-hooks.ts +33 -11
- package/src/skill-prompt.ts +3 -0
- package/src/tools/checkpoint.ts +22 -3
- package/src/tools/decision-log.ts +10 -1
- package/src/tools/defense-events.ts +298 -0
- package/src/tools/defense-store.ts +122 -0
- package/src/tools/experience-bank.ts +267 -0
- package/src/tools/experience-store.ts +169 -0
- package/src/tools/quality-gate.ts +196 -0
- package/src/tools/quality-store.ts +208 -0
- package/src/tools/transcript.ts +8 -1
- package/src/transcript.ts +11 -0
- package/src/types.ts +118 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/quality-gate.ts — quality gate query & write tool.
|
|
3
|
+
*
|
|
4
|
+
* iterate_quality_gate — query the persisted quality certificate, or compute
|
|
5
|
+
* and persist a new one from review/validation data.
|
|
6
|
+
*
|
|
7
|
+
* Provides a machine-readable quality certificate for the current iteration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
11
|
+
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
12
|
+
import { resolveProjectRootForExec } from '../config-loader.ts'
|
|
13
|
+
import { readQualityGate, writeQualityGate, computeQualityGate } from './quality-store.ts'
|
|
14
|
+
import type { QualityGateSnapshot } from '../types.ts'
|
|
15
|
+
|
|
16
|
+
/** Validate a single finding object; returns true when well-formed. */
|
|
17
|
+
function isValidFinding(raw: unknown): raw is { dimension: string; severity: string; file: string; line?: number } {
|
|
18
|
+
if (!raw || typeof raw !== 'object') return false
|
|
19
|
+
const f = raw as Record<string, unknown>
|
|
20
|
+
return (
|
|
21
|
+
typeof f.dimension === 'string' &&
|
|
22
|
+
typeof f.severity === 'string' &&
|
|
23
|
+
typeof f.file === 'string' &&
|
|
24
|
+
(f.line === undefined || typeof f.line === 'number')
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Validate a single validation result; returns true when well-formed. */
|
|
29
|
+
function isValidValidationResult(raw: unknown): raw is { command: string; exitCode: number } {
|
|
30
|
+
if (!raw || typeof raw !== 'object') return false
|
|
31
|
+
const r = raw as Record<string, unknown>
|
|
32
|
+
return typeof r.command === 'string' && typeof r.exitCode === 'number' && Number.isFinite(r.exitCode)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Sanitize a caller-supplied per-dimension number map. */
|
|
36
|
+
function sanitizeNumberMap(raw: unknown): Record<string, number> | undefined {
|
|
37
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
|
|
38
|
+
const out: Record<string, number> = {}
|
|
39
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
40
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) out[key] = value
|
|
41
|
+
}
|
|
42
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Sanitize a caller-supplied per-dimension round series map. */
|
|
46
|
+
function sanitizeRoundSeries(raw: unknown): Record<string, number[]> | undefined {
|
|
47
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
|
|
48
|
+
const out: Record<string, number[]> = {}
|
|
49
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
const series = value
|
|
52
|
+
.filter((n): n is number => typeof n === 'number' && Number.isFinite(n) && n >= 0)
|
|
53
|
+
if (series.length > 0) out[key] = series
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register the `iterate_quality_gate` tool.
|
|
61
|
+
* Reads the persisted quality certificate, or computes + persists a new one.
|
|
62
|
+
*/
|
|
63
|
+
export function registerQualityGateTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
64
|
+
ctx.tools.register(
|
|
65
|
+
defineTool({
|
|
66
|
+
name: 'iterate_quality_gate',
|
|
67
|
+
description:
|
|
68
|
+
'Query or write the quality gate status: dimension convergence rates, verification pass rates, ' +
|
|
69
|
+
'and overall PASS/FAIL status. ' +
|
|
70
|
+
'Operation "read" (default) returns the persisted machine-readable quality certificate. ' +
|
|
71
|
+
'Operation "compute" computes a fresh snapshot from this round\'s findings/validation results, ' +
|
|
72
|
+
'persists it to .iterate/quality-gate.json, and returns it.',
|
|
73
|
+
parameters: {
|
|
74
|
+
operation: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Operation: read (load persisted certificate) or compute (recompute + persist). Default: read.',
|
|
77
|
+
enum: ['read', 'compute'],
|
|
78
|
+
},
|
|
79
|
+
dimensions: {
|
|
80
|
+
type: 'array',
|
|
81
|
+
items: { type: 'string' },
|
|
82
|
+
description: 'Dimensions to gate (required for compute).',
|
|
83
|
+
},
|
|
84
|
+
findings: {
|
|
85
|
+
type: 'json',
|
|
86
|
+
description:
|
|
87
|
+
'Findings array (required for compute). Each item: { dimension, severity (critical|high|medium|low), file, line? }.',
|
|
88
|
+
},
|
|
89
|
+
validationResults: {
|
|
90
|
+
type: 'json',
|
|
91
|
+
description: 'Validation results array (optional for compute). Each item: { command, exitCode }.',
|
|
92
|
+
},
|
|
93
|
+
findingsByRound: {
|
|
94
|
+
type: 'json',
|
|
95
|
+
description:
|
|
96
|
+
'Optional per-dimension NEW-finding counts across rounds (latest last) — used to compute real convergence rates. ' +
|
|
97
|
+
'Example: { "correctness": [5, 2, 0] }.',
|
|
98
|
+
},
|
|
99
|
+
fixedByDimension: {
|
|
100
|
+
type: 'json',
|
|
101
|
+
description: 'Optional per-dimension count of fixed findings, e.g. { "correctness": 3 }.',
|
|
102
|
+
},
|
|
103
|
+
path: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Project root directory (default: current working directory).',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
output: {
|
|
110
|
+
schema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
properties: {
|
|
114
|
+
ok: { type: 'boolean', required: true },
|
|
115
|
+
kind: { type: 'string' },
|
|
116
|
+
operation: { type: 'string' },
|
|
117
|
+
snapshot: { type: 'json' },
|
|
118
|
+
error: { type: 'string' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
render: (_args, value) => {
|
|
122
|
+
if (!value.ok) return [{ type: 'text', text: `quality gate query failed: ${value.error}` }]
|
|
123
|
+
const operation = typeof value.operation === 'string' ? value.operation : 'read'
|
|
124
|
+
const snapshot = value.snapshot as unknown as QualityGateSnapshot
|
|
125
|
+
if (!snapshot) return [{ type: 'text', text: 'No quality gate data available.' }]
|
|
126
|
+
|
|
127
|
+
const statusEmoji = snapshot.overallStatus === 'pass' ? '✓' : snapshot.overallStatus === 'fail' ? '✗' : '○'
|
|
128
|
+
const lines = [
|
|
129
|
+
`${statusEmoji} Quality Gate: ${snapshot.overallStatus.toUpperCase()} (score: ${snapshot.overallScore})`,
|
|
130
|
+
`Verification: ${snapshot.passedChecks}/${snapshot.totalChecks} passed (${snapshot.verificationPassRate}%)`,
|
|
131
|
+
`Findings: ${snapshot.totalFindings} total (${snapshot.criticalCount} critical, ${snapshot.highCount} high, ${snapshot.mediumCount} medium, ${snapshot.lowCount} low)`,
|
|
132
|
+
'',
|
|
133
|
+
'Dimension Breakdown:',
|
|
134
|
+
...snapshot.dimensions.map((d) => {
|
|
135
|
+
const dimStatus = d.status === 'pass' ? '✓' : d.status === 'warn' ? '!' : '✗'
|
|
136
|
+
return ` ${dimStatus} ${d.dimension}: score=${d.score}, convergence=${d.convergenceRate}%, findings=${d.findingsCount}, fixed=${d.fixedCount}`
|
|
137
|
+
}),
|
|
138
|
+
]
|
|
139
|
+
if (snapshot.failReason) {
|
|
140
|
+
lines.push('', `Fail Reason: ${snapshot.failReason}`)
|
|
141
|
+
}
|
|
142
|
+
if (operation === 'compute') {
|
|
143
|
+
lines.push('', 'Quality gate snapshot computed and persisted.')
|
|
144
|
+
}
|
|
145
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async execute(args, exec) {
|
|
150
|
+
const resolved = resolveProjectRootForExec(exec, args.path)
|
|
151
|
+
if (!resolved.ok) return { ok: false, kind: 'quality_gate', error: resolved.reason }
|
|
152
|
+
const projectRoot = resolved.root
|
|
153
|
+
|
|
154
|
+
const operation = typeof args.operation === 'string' ? args.operation : 'read'
|
|
155
|
+
|
|
156
|
+
if (operation === 'compute') {
|
|
157
|
+
const dimensions = Array.isArray(args.dimensions)
|
|
158
|
+
? args.dimensions.filter((d): d is string => typeof d === 'string' && d.length > 0)
|
|
159
|
+
: []
|
|
160
|
+
const findings = Array.isArray(args.findings) ? args.findings.filter(isValidFinding) : []
|
|
161
|
+
const validationResults = Array.isArray(args.validationResults)
|
|
162
|
+
? args.validationResults.filter(isValidValidationResult)
|
|
163
|
+
: undefined
|
|
164
|
+
const findingsByRound = sanitizeRoundSeries(args.findingsByRound)
|
|
165
|
+
const fixedByDimension = sanitizeNumberMap(args.fixedByDimension)
|
|
166
|
+
|
|
167
|
+
const snapshot = computeQualityGate({
|
|
168
|
+
dimensions,
|
|
169
|
+
findings,
|
|
170
|
+
validationResults,
|
|
171
|
+
findingsByRound,
|
|
172
|
+
fixedByDimension,
|
|
173
|
+
})
|
|
174
|
+
const write = writeQualityGate(projectRoot, snapshot)
|
|
175
|
+
if (!write.ok) {
|
|
176
|
+
return { ok: false, kind: 'quality_gate', operation: 'compute', error: write.error }
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
kind: 'quality_gate',
|
|
181
|
+
operation: 'compute',
|
|
182
|
+
snapshot: snapshot as unknown as JsonValue,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const snapshot = readQualityGate(projectRoot)
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
kind: 'quality_gate',
|
|
190
|
+
operation: 'read',
|
|
191
|
+
snapshot: snapshot as unknown as JsonValue,
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/quality-store.ts — quality gate storage layer.
|
|
3
|
+
*
|
|
4
|
+
* Provides read/write access to quality gate data stored in
|
|
5
|
+
* .iterate/quality-gate.json. Quality gate snapshots are generated
|
|
6
|
+
* from review results and validation outcomes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs'
|
|
10
|
+
import * as path from 'node:path'
|
|
11
|
+
import type { QualityGateSnapshot, QualityGateDimension } from '../types.ts'
|
|
12
|
+
|
|
13
|
+
const QUALITY_GATE_FILE = 'quality-gate.json'
|
|
14
|
+
|
|
15
|
+
/** Default empty quality gate snapshot. */
|
|
16
|
+
function emptySnapshot(): QualityGateSnapshot {
|
|
17
|
+
return {
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
overallStatus: 'pending',
|
|
20
|
+
overallScore: 0,
|
|
21
|
+
dimensions: [],
|
|
22
|
+
verificationPassRate: 0,
|
|
23
|
+
totalChecks: 0,
|
|
24
|
+
passedChecks: 0,
|
|
25
|
+
failedChecks: 0,
|
|
26
|
+
totalFindings: 0,
|
|
27
|
+
criticalCount: 0,
|
|
28
|
+
highCount: 0,
|
|
29
|
+
mediumCount: 0,
|
|
30
|
+
lowCount: 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read the quality gate snapshot from disk. */
|
|
35
|
+
export function readQualityGate(projectRoot: string): QualityGateSnapshot {
|
|
36
|
+
const filePath = path.join(projectRoot, '.iterate', QUALITY_GATE_FILE)
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
39
|
+
const parsed = JSON.parse(content) as QualityGateSnapshot
|
|
40
|
+
if (parsed && typeof parsed === 'object') {
|
|
41
|
+
return parsed
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// File not found or invalid JSON
|
|
45
|
+
}
|
|
46
|
+
return emptySnapshot()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write the quality gate snapshot to disk.
|
|
51
|
+
* Returns `{ ok: true }` on success or `{ ok: false, error }` when the write
|
|
52
|
+
* fails — a caller must surface the failure instead of reporting success for
|
|
53
|
+
* a snapshot that was never persisted.
|
|
54
|
+
*/
|
|
55
|
+
export function writeQualityGate(
|
|
56
|
+
projectRoot: string,
|
|
57
|
+
snapshot: QualityGateSnapshot,
|
|
58
|
+
): { ok: true } | { ok: false; error: string } {
|
|
59
|
+
const dirPath = path.join(projectRoot, '.iterate')
|
|
60
|
+
const filePath = path.join(dirPath, QUALITY_GATE_FILE)
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (!fs.existsSync(dirPath)) {
|
|
64
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
65
|
+
}
|
|
66
|
+
fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf-8')
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { ok: false, error: `unable to write ${filePath}: ${String(err)}` }
|
|
69
|
+
}
|
|
70
|
+
return { ok: true }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compute the convergence rate for a dimension.
|
|
75
|
+
*
|
|
76
|
+
* Convergence measures how much NEW-finding volume shrank across rounds:
|
|
77
|
+
* `(first - last) / first` from the dimension's per-round findings series,
|
|
78
|
+
* expressed as a 0-100 percentage, clamped. A series with no fresh findings
|
|
79
|
+
* (or a dimension never reporting a first-round reading) counts as fully
|
|
80
|
+
* converged (100). Returns 0 — no measurable improvement — when a reading
|
|
81
|
+
* exists but the series is empty or malformed.
|
|
82
|
+
*/
|
|
83
|
+
export function convergenceRateFor(series: number[] | undefined, currentCount: number): number {
|
|
84
|
+
if (Array.isArray(series) && series.length > 0) {
|
|
85
|
+
const first = series.find((n) => typeof n === 'number' && Number.isFinite(n))
|
|
86
|
+
const last = [...series].reverse().find((n) => typeof n === 'number' && Number.isFinite(n))
|
|
87
|
+
if (first === undefined || last === undefined) return currentCount === 0 ? 100 : 0
|
|
88
|
+
if (first <= 0) return currentCount === 0 ? 100 : 0
|
|
89
|
+
const raw = ((first - Math.max(0, last)) / first) * 100
|
|
90
|
+
return Math.max(0, Math.min(100, Math.round(raw)))
|
|
91
|
+
}
|
|
92
|
+
return currentCount === 0 ? 100 : 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Compute a quality gate snapshot from review data. */
|
|
96
|
+
export function computeQualityGate(opts: {
|
|
97
|
+
dimensions: string[]
|
|
98
|
+
findings: Array<{
|
|
99
|
+
dimension: string
|
|
100
|
+
severity: string
|
|
101
|
+
file: string
|
|
102
|
+
line?: number
|
|
103
|
+
}>
|
|
104
|
+
validationResults?: Array<{
|
|
105
|
+
command: string
|
|
106
|
+
exitCode: number
|
|
107
|
+
}>
|
|
108
|
+
/** Per-dimension sequence of NEW-finding counts across rounds, newest last. */
|
|
109
|
+
findingsByRound?: Record<string, number[]>
|
|
110
|
+
/** Per-dimension count of findings already fixed this iteration. */
|
|
111
|
+
fixedByDimension?: Record<string, number>
|
|
112
|
+
}): QualityGateSnapshot {
|
|
113
|
+
const { dimensions, findings, validationResults, findingsByRound, fixedByDimension } = opts
|
|
114
|
+
|
|
115
|
+
// Count findings by severity
|
|
116
|
+
const criticalCount = findings.filter((f) => f.severity === 'critical').length
|
|
117
|
+
const highCount = findings.filter((f) => f.severity === 'high').length
|
|
118
|
+
const mediumCount = findings.filter((f) => f.severity === 'medium').length
|
|
119
|
+
const lowCount = findings.filter((f) => f.severity === 'low').length
|
|
120
|
+
const totalFindings = findings.length
|
|
121
|
+
|
|
122
|
+
// Compute dimension scores
|
|
123
|
+
const dimensionStats: Record<string, { count: number; critical: number; high: number; medium: number; low: number }> = {}
|
|
124
|
+
for (const dim of dimensions) {
|
|
125
|
+
dimensionStats[dim] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const finding of findings) {
|
|
129
|
+
const stats = dimensionStats[finding.dimension]
|
|
130
|
+
if (stats) {
|
|
131
|
+
stats.count++
|
|
132
|
+
if (finding.severity === 'critical') stats.critical++
|
|
133
|
+
else if (finding.severity === 'high') stats.high++
|
|
134
|
+
else if (finding.severity === 'medium') stats.medium++
|
|
135
|
+
else stats.low++
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Compute dimension-level quality gates
|
|
140
|
+
const dimensionGates: QualityGateDimension[] = dimensions.map((dim) => {
|
|
141
|
+
const stats = dimensionStats[dim] || { count: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
142
|
+
// Score: 100 - (critical*30 + high*15 + medium*5 + low*1), capped at 0
|
|
143
|
+
const penalty = stats.critical * 30 + stats.high * 15 + stats.medium * 5 + stats.low * 1
|
|
144
|
+
const score = Math.max(0, 100 - penalty)
|
|
145
|
+
const status: 'pass' | 'warn' | 'fail' = score >= 80 ? 'pass' : score >= 50 ? 'warn' : 'fail'
|
|
146
|
+
const series = findingsByRound?.[dim]
|
|
147
|
+
const convergenceRate = convergenceRateFor(Array.isArray(series) ? series : undefined, stats.count)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
dimension: dim,
|
|
151
|
+
convergenceRate,
|
|
152
|
+
findingsCount: stats.count,
|
|
153
|
+
fixedCount: fixedByDimension?.[dim] ?? 0,
|
|
154
|
+
score,
|
|
155
|
+
status,
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Compute verification pass rate
|
|
160
|
+
const totalChecks = validationResults?.length ?? 0
|
|
161
|
+
const passedChecks = validationResults?.filter((r) => r.exitCode === 0).length ?? 0
|
|
162
|
+
const failedChecks = totalChecks - passedChecks
|
|
163
|
+
const verificationPassRate = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0
|
|
164
|
+
|
|
165
|
+
// Compute overall score (weighted average of dimension scores)
|
|
166
|
+
const overallScore = dimensionGates.length > 0
|
|
167
|
+
? Math.round(dimensionGates.reduce((sum, d) => sum + d.score, 0) / dimensionGates.length)
|
|
168
|
+
: 0
|
|
169
|
+
|
|
170
|
+
// Determine overall status
|
|
171
|
+
const hasCritical = criticalCount > 0
|
|
172
|
+
const hasHighFail = dimensionGates.some((d) => d.status === 'fail')
|
|
173
|
+
const verificationFails = totalChecks > 0 && failedChecks > 0
|
|
174
|
+
|
|
175
|
+
let overallStatus: 'pass' | 'fail' | 'pending' = 'pass'
|
|
176
|
+
let failReason: string | undefined
|
|
177
|
+
|
|
178
|
+
if (hasCritical) {
|
|
179
|
+
overallStatus = 'fail'
|
|
180
|
+
failReason = `${criticalCount} critical findings present`
|
|
181
|
+
} else if (hasHighFail) {
|
|
182
|
+
overallStatus = 'fail'
|
|
183
|
+
failReason = 'One or more dimensions failed quality gate'
|
|
184
|
+
} else if (verificationFails) {
|
|
185
|
+
overallStatus = 'fail'
|
|
186
|
+
failReason = `${failedChecks} validation checks failed`
|
|
187
|
+
} else if (overallScore < 70) {
|
|
188
|
+
overallStatus = 'fail'
|
|
189
|
+
failReason = `Overall score ${overallScore} below threshold (70)`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
overallStatus,
|
|
195
|
+
overallScore,
|
|
196
|
+
dimensions: dimensionGates,
|
|
197
|
+
verificationPassRate,
|
|
198
|
+
totalChecks,
|
|
199
|
+
passedChecks,
|
|
200
|
+
failedChecks,
|
|
201
|
+
failReason,
|
|
202
|
+
totalFindings,
|
|
203
|
+
criticalCount,
|
|
204
|
+
highCount,
|
|
205
|
+
mediumCount,
|
|
206
|
+
lowCount,
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/tools/transcript.ts
CHANGED
|
@@ -136,6 +136,11 @@ export function registerTranscriptTool(ctx: {
|
|
|
136
136
|
description: 'For `capture`: run mode ("dry-run" | "normal"). Default dry-run.',
|
|
137
137
|
enum: ['dry-run', 'normal'],
|
|
138
138
|
},
|
|
139
|
+
taskMode: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'For `capture`: harness execution mode ("code" | "iterate"). Default derives from the review loop (iterate).',
|
|
142
|
+
enum: ['code', 'iterate'],
|
|
143
|
+
},
|
|
139
144
|
goal: { type: 'string', description: 'For `capture`: run goal.' },
|
|
140
145
|
maxRounds: { type: 'integer', description: 'For `capture`: round cap.' },
|
|
141
146
|
roundsExecuted: { type: 'integer', description: 'For `capture`: number of rounds actually executed.' },
|
|
@@ -229,10 +234,11 @@ export function registerTranscriptTool(ctx: {
|
|
|
229
234
|
|
|
230
235
|
// capture
|
|
231
236
|
const mode = args.mode === 'normal' ? 'normal' : 'dry-run'
|
|
237
|
+
const taskMode = args.taskMode === 'code' || args.taskMode === 'iterate' ? args.taskMode : undefined
|
|
232
238
|
const goal = typeof args.goal === 'string' ? args.goal : ''
|
|
233
239
|
const maxRounds =
|
|
234
240
|
typeof args.maxRounds === 'number' ? Math.floor(args.maxRounds) : 0
|
|
235
|
-
const builder = new ReviewTranscriptBuilder({ project: projectRoot, mode, approval, goal, maxRounds })
|
|
241
|
+
const builder = new ReviewTranscriptBuilder({ project: projectRoot, mode, taskMode, approval, goal, maxRounds })
|
|
236
242
|
const report = args.report as Record<string, unknown> | null | undefined
|
|
237
243
|
const reportFindings: unknown =
|
|
238
244
|
report && typeof report === 'object' && Array.isArray(report.findings)
|
|
@@ -300,6 +306,7 @@ function rehydrateBuilder(manifest: TranscriptManifest, approval: 'ask' | 'deny'
|
|
|
300
306
|
const builder = new ReviewTranscriptBuilder({
|
|
301
307
|
project: manifest.project,
|
|
302
308
|
mode: manifest.mode ?? null,
|
|
309
|
+
taskMode: manifest.taskMode ?? null,
|
|
303
310
|
approval,
|
|
304
311
|
goal: manifest.goal,
|
|
305
312
|
maxRounds: manifest.maxRounds,
|
package/src/transcript.ts
CHANGED
|
@@ -126,6 +126,7 @@ function mergeReportIntoThread(
|
|
|
126
126
|
export class ReviewTranscriptBuilder {
|
|
127
127
|
private readonly project: string
|
|
128
128
|
private readonly mode: 'dry-run' | 'normal' | null
|
|
129
|
+
private readonly taskMode: 'code' | 'iterate' | null
|
|
129
130
|
private readonly approval: 'ask' | 'deny' | 'allow'
|
|
130
131
|
private goal = ''
|
|
131
132
|
private readonly phases: string[] = []
|
|
@@ -144,6 +145,7 @@ export class ReviewTranscriptBuilder {
|
|
|
144
145
|
constructor(input: {
|
|
145
146
|
project: string
|
|
146
147
|
mode?: 'dry-run' | 'normal' | null
|
|
148
|
+
taskMode?: 'code' | 'iterate' | null
|
|
147
149
|
approval?: 'ask' | 'deny' | 'allow'
|
|
148
150
|
goal?: string
|
|
149
151
|
maxRounds?: number
|
|
@@ -152,6 +154,14 @@ export class ReviewTranscriptBuilder {
|
|
|
152
154
|
this.project = input.project || ''
|
|
153
155
|
this.mode =
|
|
154
156
|
input.mode === 'dry-run' || input.mode === 'normal' ? input.mode : null
|
|
157
|
+
// v3.0: task_mode indicator. An explicit valid value wins; otherwise a
|
|
158
|
+
// run that exercises the review loop (any mode) defaults to "iterate".
|
|
159
|
+
this.taskMode =
|
|
160
|
+
input.taskMode === 'code' || input.taskMode === 'iterate'
|
|
161
|
+
? input.taskMode
|
|
162
|
+
: input.mode !== null && input.mode !== undefined
|
|
163
|
+
? 'iterate'
|
|
164
|
+
: null
|
|
155
165
|
this.approval =
|
|
156
166
|
input.approval === 'ask' || input.approval === 'deny' || input.approval === 'allow'
|
|
157
167
|
? input.approval
|
|
@@ -393,6 +403,7 @@ export class ReviewTranscriptBuilder {
|
|
|
393
403
|
updatedAt: this.updatedAt,
|
|
394
404
|
active: this.active,
|
|
395
405
|
mode: this.mode,
|
|
406
|
+
taskMode: this.taskMode,
|
|
396
407
|
goal: this.goal,
|
|
397
408
|
phases: this.phases,
|
|
398
409
|
round: this.round,
|
package/src/types.ts
CHANGED
|
@@ -230,6 +230,20 @@ export interface IterationStatus {
|
|
|
230
230
|
resumeCount: number
|
|
231
231
|
checkpoint: IterationCheckpoint | null
|
|
232
232
|
lastUpdated: string | null
|
|
233
|
+
/** v3.0: Quality gate snapshot */
|
|
234
|
+
qualityGate?: QualityGateSnapshot
|
|
235
|
+
/** v3.0: Experience bank summary */
|
|
236
|
+
experienceBank?: {
|
|
237
|
+
totalEntries: number
|
|
238
|
+
totalHits: number
|
|
239
|
+
}
|
|
240
|
+
/** v3.0: Defense events summary */
|
|
241
|
+
defenseEvents?: {
|
|
242
|
+
totalEvents: number
|
|
243
|
+
counts: Record<DefenseEventType, number>
|
|
244
|
+
}
|
|
245
|
+
/** v3.0: task_mode from harness */
|
|
246
|
+
taskMode?: 'code' | 'iterate' | null
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
/** ─── Runtime observatory (transcript) ───────────────────────────────────── */
|
|
@@ -331,4 +345,108 @@ export interface TranscriptManifest {
|
|
|
331
345
|
active: boolean
|
|
332
346
|
policy: 'ask' | 'deny' | 'allow'
|
|
333
347
|
}
|
|
348
|
+
/** v3.0: task_mode indicator from harness status */
|
|
349
|
+
taskMode?: 'code' | 'iterate' | null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─── v3.0: Quality Gate ──────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/** A single dimension's quality gate status. */
|
|
355
|
+
export interface QualityGateDimension {
|
|
356
|
+
dimension: string
|
|
357
|
+
convergenceRate: number
|
|
358
|
+
findingsCount: number
|
|
359
|
+
fixedCount: number
|
|
360
|
+
/** 0-100 score based on findings severity and count */
|
|
361
|
+
score: number
|
|
362
|
+
status: 'pass' | 'warn' | 'fail'
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Quality gate snapshot for the current iteration. */
|
|
366
|
+
export interface QualityGateSnapshot {
|
|
367
|
+
timestamp: string
|
|
368
|
+
overallStatus: 'pass' | 'fail' | 'pending'
|
|
369
|
+
overallScore: number
|
|
370
|
+
dimensions: QualityGateDimension[]
|
|
371
|
+
verificationPassRate: number
|
|
372
|
+
totalChecks: number
|
|
373
|
+
passedChecks: number
|
|
374
|
+
failedChecks: number
|
|
375
|
+
/** Reason for overall FAIL status, if applicable */
|
|
376
|
+
failReason?: string
|
|
377
|
+
/** Total findings across all dimensions */
|
|
378
|
+
totalFindings: number
|
|
379
|
+
/** Findings by severity */
|
|
380
|
+
criticalCount: number
|
|
381
|
+
highCount: number
|
|
382
|
+
mediumCount: number
|
|
383
|
+
lowCount: number
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─── v3.0: Experience Bank ───────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
/** A single experience entry in the experience bank. */
|
|
389
|
+
export interface ExperienceEntry {
|
|
390
|
+
id: string
|
|
391
|
+
timestamp: string
|
|
392
|
+
dimension: string
|
|
393
|
+
pattern: string
|
|
394
|
+
description: string
|
|
395
|
+
/** The fix that was applied and verified */
|
|
396
|
+
verifiedFix: string
|
|
397
|
+
/** Files involved in this experience */
|
|
398
|
+
files: string[]
|
|
399
|
+
/** How many times this pattern has been encountered */
|
|
400
|
+
hitCount: number
|
|
401
|
+
/** Last time this experience was hit */
|
|
402
|
+
lastHitAt?: string
|
|
403
|
+
/** Tags for categorization */
|
|
404
|
+
tags: string[]
|
|
405
|
+
/** Related finding summary */
|
|
406
|
+
findingSummary: string
|
|
407
|
+
/** Severity of the original finding */
|
|
408
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Experience bank state for the project. */
|
|
412
|
+
export interface ExperienceBank {
|
|
413
|
+
entries: ExperienceEntry[]
|
|
414
|
+
lastUpdated: string
|
|
415
|
+
totalHits: number
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── v3.0: Defense Events ────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
/** Defense event types */
|
|
421
|
+
export type DefenseEventType =
|
|
422
|
+
| 'precondition_failed'
|
|
423
|
+
| 'rollback'
|
|
424
|
+
| 'invariant_violated'
|
|
425
|
+
| 'assumption_falsified'
|
|
426
|
+
|
|
427
|
+
/** A single defense event recorded during iteration. */
|
|
428
|
+
export interface DefenseEvent {
|
|
429
|
+
id: string
|
|
430
|
+
timestamp: string
|
|
431
|
+
round: number
|
|
432
|
+
type: DefenseEventType
|
|
433
|
+
/** What was being checked */
|
|
434
|
+
description: string
|
|
435
|
+
/** The defense that was triggered */
|
|
436
|
+
defense: string
|
|
437
|
+
/** Outcome: what was protected against */
|
|
438
|
+
outcome: string
|
|
439
|
+
/** Optional file/location context */
|
|
440
|
+
file?: string
|
|
441
|
+
line?: number
|
|
442
|
+
/** Severity of the event */
|
|
443
|
+
severity: 'critical' | 'high' | 'medium' | 'low'
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Defense events stream for the current iteration. */
|
|
447
|
+
export interface DefenseEventStream {
|
|
448
|
+
events: DefenseEvent[]
|
|
449
|
+
lastUpdated: string
|
|
450
|
+
/** Summary counts by type */
|
|
451
|
+
counts: Record<DefenseEventType, number>
|
|
334
452
|
}
|