iterate-plugin 2.12.3 → 3.2.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 +28 -13
- package/dist/approval-gate.js +16 -2
- 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/decision-log.js +10 -1
- package/dist/tools/defense-events.js +260 -0
- package/dist/tools/defense-store.js +97 -0
- package/dist/tools/experience-bank.js +248 -0
- package/dist/tools/experience-store.js +132 -0
- package/dist/tools/quality-gate.js +180 -0
- package/dist/tools/quality-store.js +174 -0
- package/lib/client.js +318 -15
- package/package.json +6 -6
- package/src/approval-gate.ts +14 -2
- package/src/client/index.ts +288 -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/decision-log.ts +10 -1
- package/src/tools/defense-events.ts +295 -0
- package/src/tools/defense-store.ts +113 -0
- package/src/tools/experience-bank.ts +264 -0
- package/src/tools/experience-store.ts +160 -0
- package/src/tools/quality-gate.ts +193 -0
- package/src/tools/quality-store.ts +199 -0
- package/src/types.ts +118 -0
package/src/approval-gate.ts
CHANGED
|
@@ -65,11 +65,23 @@ export function decideApproval(
|
|
|
65
65
|
execution: ToolExecutionLike,
|
|
66
66
|
policy: 'ask' | 'deny' | 'allow',
|
|
67
67
|
): ApprovalDecision {
|
|
68
|
-
|
|
68
|
+
// Defensive reads: a hostile/proxied execution object must degrade to "not
|
|
69
|
+
// our tool" (allow) rather than throw inside the gate.
|
|
70
|
+
let name = ''
|
|
71
|
+
try {
|
|
72
|
+
name = typeof execution?.name === 'string' ? execution.name : ''
|
|
73
|
+
} catch {
|
|
74
|
+
name = ''
|
|
75
|
+
}
|
|
69
76
|
if (!name) return { kind: 'allow' }
|
|
70
77
|
if (!DESTRUCTIVE_TOOLS.has(name)) return { kind: 'allow' }
|
|
71
78
|
|
|
72
|
-
|
|
79
|
+
let rawArgs: unknown
|
|
80
|
+
try {
|
|
81
|
+
rawArgs = execution.arguments
|
|
82
|
+
} catch {
|
|
83
|
+
rawArgs = undefined
|
|
84
|
+
}
|
|
73
85
|
const args: Record<string, unknown> =
|
|
74
86
|
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
75
87
|
? (rawArgs as Record<string, unknown>)
|
package/src/client/index.ts
CHANGED
|
@@ -435,6 +435,43 @@ const ITERATE_CSS = `
|
|
|
435
435
|
.iterate-chip-phase { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-label-primary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 14%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 26%, transparent); }
|
|
436
436
|
.iterate-chip-phase[data-live] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
437
437
|
|
|
438
|
+
/* v3.0: task_mode indicator chip */
|
|
439
|
+
.iterate-chip-taskmode { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
440
|
+
.iterate-chip-taskmode[data-mode="code"] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
441
|
+
.iterate-chip-taskmode[data-mode="iterate"] { color: #B45309; background: color-mix(in srgb, #B45309 12%, transparent); border: 1px solid color-mix(in srgb, #B45309 28%, transparent); }
|
|
442
|
+
|
|
443
|
+
/* v3.0: Quality gate status */
|
|
444
|
+
.iterate-gate { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
445
|
+
.iterate-gate[data-status="pass"] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
446
|
+
.iterate-gate[data-status="fail"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
447
|
+
.iterate-gate[data-status="pending"] { color: var(--dsw-alias-label-secondary); background: color-mix(in srgb, var(--dsw-alias-label-secondary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-label-secondary) 28%, transparent); }
|
|
448
|
+
|
|
449
|
+
/* v3.0: Experience bank hit highlight */
|
|
450
|
+
.iterate-exp-hit { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); font-size: 11px; color: var(--dsw-alias-state-success-primary); }
|
|
451
|
+
.iterate-exp-hit b { font-weight: 600; }
|
|
452
|
+
|
|
453
|
+
/* v3.0: Defense event type badges */
|
|
454
|
+
.iterate-defense-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
455
|
+
.iterate-defense-badge[data-type="precondition_failed"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
456
|
+
.iterate-defense-badge[data-type="rollback"] { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
457
|
+
.iterate-defense-badge[data-type="invariant_violated"] { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent); }
|
|
458
|
+
.iterate-defense-badge[data-type="assumption_falsified"] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
459
|
+
|
|
460
|
+
/* v3.0: Command buttons */
|
|
461
|
+
.iterate-cmd { padding: 4px 10px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 11px; cursor: pointer; font-weight: 500; }
|
|
462
|
+
.iterate-cmd:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
463
|
+
.iterate-cmd[data-primary] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
|
|
464
|
+
.iterate-cmd[data-danger] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); color: var(--dsw-alias-state-error-primary); }
|
|
465
|
+
.iterate-cmd:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
466
|
+
|
|
467
|
+
/* v3.0: Dimension score bar */
|
|
468
|
+
.iterate-dim-score { display: flex; align-items: center; gap: 6px; font-size: 11px; }
|
|
469
|
+
.iterate-dim-score-bar { width: 60px; height: 6px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
|
|
470
|
+
.iterate-dim-score-fill { height: 100%; border-radius: 3px; transition: width 300ms ease; }
|
|
471
|
+
.iterate-dim-score-fill[data-status="pass"] { background: var(--dsw-alias-state-success-primary); }
|
|
472
|
+
.iterate-dim-score-fill[data-status="warn"] { background: var(--dsw-alias-state-warn-primary); }
|
|
473
|
+
.iterate-dim-score-fill[data-status="fail"] { background: var(--dsw-alias-state-error-primary); }
|
|
474
|
+
|
|
438
475
|
/* Accessibility-switch toggle */
|
|
439
476
|
.iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
|
|
440
477
|
.iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
|
|
@@ -795,6 +832,19 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
795
832
|
}, `${phase} · ${transcript.active === true ? '运行中' : '已结束'}`)
|
|
796
833
|
: null
|
|
797
834
|
|
|
835
|
+
// v3.0: task_mode indicator (code/iterate)
|
|
836
|
+
// task_mode is read from the session transcript if available
|
|
837
|
+
const taskModeTranscript = latestTranscript(session)
|
|
838
|
+
const taskMode = (taskModeTranscript as unknown as { taskMode?: string })?.taskMode
|
|
839
|
+
const taskModeChip = taskMode
|
|
840
|
+
? React.createElement('span', {
|
|
841
|
+
className: 'iterate-chip-taskmode',
|
|
842
|
+
key: 'taskmode',
|
|
843
|
+
'data-mode': taskMode,
|
|
844
|
+
title: taskMode === 'code' ? '代码模式:直接修改文件' : '迭代模式:审查-修复循环',
|
|
845
|
+
}, taskMode === 'code' ? '● code' : '● iterate')
|
|
846
|
+
: null
|
|
847
|
+
|
|
798
848
|
const dimNames = Object.keys(dims)
|
|
799
849
|
const dimBadges = dimNames.slice(0, 6).map((dim) =>
|
|
800
850
|
React.createElement(
|
|
@@ -869,6 +919,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
869
919
|
resumeChip,
|
|
870
920
|
imageChip,
|
|
871
921
|
liveChip,
|
|
922
|
+
taskModeChip,
|
|
872
923
|
React.createElement(TrendChart, { points: trend.points }),
|
|
873
924
|
...dimBadges,
|
|
874
925
|
)
|
|
@@ -974,6 +1025,8 @@ function TriagePanel(props: SlotProps) {
|
|
|
974
1025
|
const [filter, setFilter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>({ severities: [], dimensions: [], search: '' })
|
|
975
1026
|
const [selected, setSelected] = React.useState<number | null>(null)
|
|
976
1027
|
const [selectAll, setSelectAll] = React.useState(false)
|
|
1028
|
+
// v3.0: Command mode for native action buttons
|
|
1029
|
+
const [cmdCopied, setCmdCopied] = React.useState<string | null>(null)
|
|
977
1030
|
|
|
978
1031
|
/** Persist the verdicts to localStorage whenever they change. Kept OUT of
|
|
979
1032
|
* the state updater (updaters must stay pure — React may double-invoke them
|
|
@@ -1059,6 +1112,37 @@ function TriagePanel(props: SlotProps) {
|
|
|
1059
1112
|
const ignoredCount = ignored.length
|
|
1060
1113
|
const counts = countVerdicts(verdicts)
|
|
1061
1114
|
|
|
1115
|
+
// v3.0: Command handlers for native action buttons
|
|
1116
|
+
const doApproveArchitecturalFix = () => {
|
|
1117
|
+
const cmd = '请调用 `iterate_fix` 批准架构修复(设置 is_architectural: true)'
|
|
1118
|
+
copyText(cmd).then((ok) => {
|
|
1119
|
+
if (ok) {
|
|
1120
|
+
setCmdCopied('approve-arch')
|
|
1121
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const doTriggerNewRound = () => {
|
|
1127
|
+
const cmd = '请继续执行下一轮迭代审查'
|
|
1128
|
+
copyText(cmd).then((ok) => {
|
|
1129
|
+
if (ok) {
|
|
1130
|
+
setCmdCopied('new-round')
|
|
1131
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1132
|
+
}
|
|
1133
|
+
})
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const doRollbackToCheckpoint = () => {
|
|
1137
|
+
const cmd = '请调用 `iterate_checkpoint` 回滚到上一个检查点'
|
|
1138
|
+
copyText(cmd).then((ok) => {
|
|
1139
|
+
if (ok) {
|
|
1140
|
+
setCmdCopied('rollback')
|
|
1141
|
+
setTimeout(() => setCmdCopied(null), 1600)
|
|
1142
|
+
}
|
|
1143
|
+
})
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1062
1146
|
const doCopyYaml = () => {
|
|
1063
1147
|
const yaml = toKnownIntentionalYaml(ignored)
|
|
1064
1148
|
if (!yaml) return
|
|
@@ -1191,6 +1275,27 @@ function TriagePanel(props: SlotProps) {
|
|
|
1191
1275
|
}, '生成应用指令'),
|
|
1192
1276
|
),
|
|
1193
1277
|
),
|
|
1278
|
+
// v3.0: Command buttons for native actions (§8)
|
|
1279
|
+
React.createElement('div', { className: 'iterate-triage-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1280
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, '指挥操作'),
|
|
1281
|
+
React.createElement('span', { style: { display: 'flex', gap: 6 } },
|
|
1282
|
+
React.createElement('button', {
|
|
1283
|
+
className: 'iterate-cmd', 'data-primary': '', 'data-copied': cmdCopied === 'approve-arch' ? '' : undefined,
|
|
1284
|
+
onClick: doApproveArchitecturalFix,
|
|
1285
|
+
title: '批准架构修复(复制指令文本)',
|
|
1286
|
+
}, cmdCopied === 'approve-arch' ? '已复制' : '批准架构修复'),
|
|
1287
|
+
React.createElement('button', {
|
|
1288
|
+
className: 'iterate-cmd', 'data-copied': cmdCopied === 'new-round' ? '' : undefined,
|
|
1289
|
+
onClick: doTriggerNewRound,
|
|
1290
|
+
title: '触发新一轮迭代审查',
|
|
1291
|
+
}, cmdCopied === 'new-round' ? '已复制' : '触发新一轮'),
|
|
1292
|
+
React.createElement('button', {
|
|
1293
|
+
className: 'iterate-cmd', 'data-danger': '', 'data-copied': cmdCopied === 'rollback' ? '' : undefined,
|
|
1294
|
+
onClick: doRollbackToCheckpoint,
|
|
1295
|
+
title: '回滚到上一个检查点',
|
|
1296
|
+
}, cmdCopied === 'rollback' ? '已复制' : '回滚检查点'),
|
|
1297
|
+
),
|
|
1298
|
+
),
|
|
1194
1299
|
payload
|
|
1195
1300
|
? React.createElement('div', { className: 'iterate-payload' }, payload)
|
|
1196
1301
|
: null,
|
|
@@ -1281,7 +1386,13 @@ function ProgressCapsule() {
|
|
|
1281
1386
|
}
|
|
1282
1387
|
}, [])
|
|
1283
1388
|
if (!info) return null
|
|
1284
|
-
return React.createElement('div', {
|
|
1389
|
+
return React.createElement('div', {
|
|
1390
|
+
'data-iterate-root': '',
|
|
1391
|
+
className: 'iterate-capsule',
|
|
1392
|
+
'data-ok': info.ok ? '' : undefined,
|
|
1393
|
+
role: 'status',
|
|
1394
|
+
'aria-live': 'polite',
|
|
1395
|
+
}, info.text)
|
|
1285
1396
|
}
|
|
1286
1397
|
|
|
1287
1398
|
/** Settings page section (root scope; reads localStorage + latest known data). */
|
|
@@ -1452,6 +1563,10 @@ const OBS_TABS = [
|
|
|
1452
1563
|
{ key: 'f5', label: '断点恢复' },
|
|
1453
1564
|
{ key: 'f6', label: '运行控制台' },
|
|
1454
1565
|
{ key: 'f7', label: '决策时间线' },
|
|
1566
|
+
// v3.0: Quality Command Center tabs
|
|
1567
|
+
{ key: 'f8', label: '质量门禁' },
|
|
1568
|
+
{ key: 'f9', label: '经验银行' },
|
|
1569
|
+
{ key: 'f10', label: '防御事件' },
|
|
1455
1570
|
] as const
|
|
1456
1571
|
|
|
1457
1572
|
/** Live subagent activity badges: type → Chinese label + dsw token color accent. */
|
|
@@ -1543,6 +1658,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1543
1658
|
const [f3Filter, setF3Filter] = React.useState<{ severities: string[]; dimensions: string[]; search: string }>(
|
|
1544
1659
|
{ severities: [], dimensions: [], search: '' },
|
|
1545
1660
|
)
|
|
1661
|
+
// v3.0: Experience bank search state
|
|
1662
|
+
const [expSearch, setExpSearch] = React.useState('')
|
|
1663
|
+
const [expResults, setExpResults] = React.useState<unknown[]>([])
|
|
1664
|
+
// v3.0: Defense events filter
|
|
1665
|
+
const [defenseFilter, setDefenseFilter] = React.useState('')
|
|
1546
1666
|
|
|
1547
1667
|
// Single shared "copied" flash timer (reused across all copy buttons).
|
|
1548
1668
|
const copyTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -1571,6 +1691,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1571
1691
|
|
|
1572
1692
|
/** Export the full observatory snapshot as JSON (download, then copy fallback). */
|
|
1573
1693
|
const exportObservatory = () => {
|
|
1694
|
+
if (!manifest) return
|
|
1574
1695
|
const json = serializeObservatoryExport(manifest, manifest.live || [])
|
|
1575
1696
|
const filename = `iterate-observatory-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
|
|
1576
1697
|
const flash = () => {
|
|
@@ -1722,8 +1843,8 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1722
1843
|
if (findings.length === 0) {
|
|
1723
1844
|
return React.createElement('div', { className: 'iterate-obs-empty' }, '暂无发现')
|
|
1724
1845
|
}
|
|
1725
|
-
const opts = buildFilterOptions(findings)
|
|
1726
|
-
const { filtered } = filterFindingsWithIndices(findings, f3Filter)
|
|
1846
|
+
const opts = buildFilterOptions(findings as unknown as Record<string, unknown>[])
|
|
1847
|
+
const { filtered } = filterFindingsWithIndices(findings as unknown as Record<string, unknown>[], f3Filter)
|
|
1727
1848
|
const filterActive = f3Filter.severities.length > 0 || f3Filter.dimensions.length > 0 || f3Filter.search.trim() !== ''
|
|
1728
1849
|
const toggleSeverity = (sev: string) => {
|
|
1729
1850
|
setF3Filter((prev) => ({
|
|
@@ -1778,31 +1899,32 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
1778
1899
|
)
|
|
1779
1900
|
const findingBlocks = filtered.map((f, i) => {
|
|
1780
1901
|
const k = `f3-${i}`
|
|
1781
|
-
const
|
|
1902
|
+
const finding = f as unknown as ObsFinding
|
|
1903
|
+
const loc = `${String(finding.file || '?')}${typeof finding.line === 'number' && finding.line > 0 ? `:${finding.line}` : ''}`
|
|
1782
1904
|
const entry = {
|
|
1783
|
-
file: String(
|
|
1784
|
-
...(typeof
|
|
1785
|
-
dimension: String(
|
|
1786
|
-
reason: String(
|
|
1905
|
+
file: String(finding.file || ''),
|
|
1906
|
+
...(typeof finding.line === 'number' && finding.line > 0 ? { line: finding.line } : {}),
|
|
1907
|
+
dimension: String(finding.dimension || ''),
|
|
1908
|
+
reason: String(finding.summary || ''),
|
|
1787
1909
|
}
|
|
1788
1910
|
const knownInstruction = buildApplyInstruction([entry])
|
|
1789
|
-
const fixInstruction = buildObsFixInstruction(
|
|
1911
|
+
const fixInstruction = buildObsFixInstruction(finding)
|
|
1790
1912
|
return React.createElement('div', { key: k, className: 'iterate-obs-block' },
|
|
1791
1913
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
1792
|
-
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(
|
|
1793
|
-
React.createElement('span', {}, severityLabel(
|
|
1914
|
+
React.createElement('span', { className: 'iterate-sev-dot', style: { background: severityColor(finding.severity) } }),
|
|
1915
|
+
React.createElement('span', {}, severityLabel(finding.severity)),
|
|
1794
1916
|
React.createElement('span', { className: 'iterate-obs-file' }, loc),
|
|
1795
|
-
React.createElement('span', {}, String(
|
|
1796
|
-
|
|
1917
|
+
React.createElement('span', {}, String(finding.dimension || '')),
|
|
1918
|
+
finding.acknowledged === true
|
|
1797
1919
|
? React.createElement('span', { className: 'iterate-obs-chip' }, '已确认')
|
|
1798
1920
|
: null,
|
|
1799
1921
|
),
|
|
1800
1922
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
1801
|
-
React.createElement('div', { className: 'iterate-obs-msg' }, String(
|
|
1923
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, String(finding.summary || '')),
|
|
1802
1924
|
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
1803
1925
|
React.createElement('button', {
|
|
1804
1926
|
className: 'iterate-btn', 'data-copied': copiedKey === `${k}-fix` ? '' : undefined,
|
|
1805
|
-
onClick: () => copyInstruction(`${k}-fix`, String(
|
|
1927
|
+
onClick: () => copyInstruction(`${k}-fix`, String(finding.suggested_fix || '')),
|
|
1806
1928
|
title: '复制该 finding 的建议修复文本',
|
|
1807
1929
|
}, copiedKey === `${k}-fix` ? '已复制' : '复制 suggested_fix'),
|
|
1808
1930
|
React.createElement('button', {
|
|
@@ -2033,6 +2155,152 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2033
2155
|
return React.createElement('div', {}, filterBar, ...rows)
|
|
2034
2156
|
}
|
|
2035
2157
|
|
|
2158
|
+
// ── F8: Quality Gate ──────────────────────────────────────────────────────
|
|
2159
|
+
// v3.0: Quality command center - shows dimension convergence, verification pass rate, and overall PASS/FAIL
|
|
2160
|
+
const renderQualityGate = () => {
|
|
2161
|
+
// Quality gate data would come from manifest or be computed on-the-fly
|
|
2162
|
+
// For now, show a placeholder with instructions
|
|
2163
|
+
const qualityGateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
|
|
2164
|
+
return React.createElement('div', {},
|
|
2165
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2166
|
+
React.createElement('b', {}, '质量门禁'),
|
|
2167
|
+
React.createElement('button', {
|
|
2168
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'qgate' ? '' : undefined,
|
|
2169
|
+
onClick: () => copyInstruction('qgate', qualityGateInstruction),
|
|
2170
|
+
title: '复制 iterate_quality_gate 查询指令',
|
|
2171
|
+
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2172
|
+
),
|
|
2173
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2174
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2175
|
+
React.createElement('span', {}, '质量门禁视图'),
|
|
2176
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2177
|
+
),
|
|
2178
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2179
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, '质量门禁显示各维度收敛度、验证通过率和整体 PASS/FAIL 状态。'),
|
|
2180
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2181
|
+
React.createElement('button', {
|
|
2182
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'qgate-instr' ? '' : undefined,
|
|
2183
|
+
onClick: () => copyInstruction('qgate-instr', qualityGateInstruction),
|
|
2184
|
+
title: '复制查询指令',
|
|
2185
|
+
}, copiedKey === 'qgate-instr' ? '已复制' : '复制查询指令'),
|
|
2186
|
+
),
|
|
2187
|
+
),
|
|
2188
|
+
),
|
|
2189
|
+
)
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// ── F9: Experience Bank ────────────────────────────────────────────────────
|
|
2193
|
+
// v3.0: Browse/search historical fixes and patterns
|
|
2194
|
+
const renderExperienceBank = () => {
|
|
2195
|
+
const expInstruction = expSearch
|
|
2196
|
+
? `请调用 \`iterate_experience\` 搜索经验:\n\n\`\`\`json\n${JSON.stringify({ operation: 'search', query: expSearch }, null, 2)}\n\`\`\``
|
|
2197
|
+
: '请调用 `iterate_experience` 列出所有经验'
|
|
2198
|
+
|
|
2199
|
+
const resultRows = expResults.map((entry: unknown, i: number) => {
|
|
2200
|
+
const e = entry as { id?: string; pattern?: string; description?: string; hitCount?: number; dimension?: string }
|
|
2201
|
+
return React.createElement('div', { key: `exp-${i}`, className: 'iterate-obs-row' },
|
|
2202
|
+
React.createElement('span', { className: 'iterate-obs-chip' }, String(e.dimension || '')),
|
|
2203
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, String(e.pattern || '')),
|
|
2204
|
+
React.createElement('span', { className: 'iterate-exp-hit' }, `命中 ${e.hitCount ?? 0} 次`),
|
|
2205
|
+
)
|
|
2206
|
+
})
|
|
2207
|
+
|
|
2208
|
+
return React.createElement('div', {},
|
|
2209
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2210
|
+
React.createElement('b', {}, '经验银行'),
|
|
2211
|
+
React.createElement('button', {
|
|
2212
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'exp-list' ? '' : undefined,
|
|
2213
|
+
onClick: () => copyInstruction('exp-list', '请调用 `iterate_experience` 列出所有经验'),
|
|
2214
|
+
title: '复制列出经验指令',
|
|
2215
|
+
}, copiedKey === 'exp-list' ? '已复制' : '列出经验'),
|
|
2216
|
+
),
|
|
2217
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2218
|
+
React.createElement('input', {
|
|
2219
|
+
className: 'iterate-filter-search',
|
|
2220
|
+
type: 'search',
|
|
2221
|
+
placeholder: '搜索经验模式…',
|
|
2222
|
+
'aria-label': '搜索经验',
|
|
2223
|
+
value: expSearch,
|
|
2224
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setExpSearch(e.target.value),
|
|
2225
|
+
}),
|
|
2226
|
+
React.createElement('button', {
|
|
2227
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'exp-search' ? '' : undefined,
|
|
2228
|
+
disabled: !expSearch,
|
|
2229
|
+
onClick: () => copyInstruction('exp-search', expInstruction),
|
|
2230
|
+
title: '复制搜索指令',
|
|
2231
|
+
}, copiedKey === 'exp-search' ? '已复制' : '搜索'),
|
|
2232
|
+
),
|
|
2233
|
+
resultRows.length > 0
|
|
2234
|
+
? React.createElement('div', {}, ...resultRows)
|
|
2235
|
+
: React.createElement('div', { className: 'iterate-obs-empty' }, '输入关键词搜索历史经验,或点击「列出经验」查看全部'),
|
|
2236
|
+
)
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// ── F10: Defense Events ────────────────────────────────────────────────────
|
|
2240
|
+
// v3.0: Shows precondition failures, rollbacks, invariant violations, assumption falsifications
|
|
2241
|
+
const renderDefenseEvents = () => {
|
|
2242
|
+
const defenseInstruction = defenseFilter
|
|
2243
|
+
? `请调用 \`iterate_defense_events\` 查询防御事件:\n\n\`\`\`json\n${JSON.stringify({ operation: 'list', type: defenseFilter || undefined }, null, 2)}\n\`\`\``
|
|
2244
|
+
: '请调用 `iterate_defense_events` 列出所有防御事件'
|
|
2245
|
+
|
|
2246
|
+
const typeOptions = [
|
|
2247
|
+
{ value: '', label: '全部类型' },
|
|
2248
|
+
{ value: 'precondition_failed', label: '前置校验失败' },
|
|
2249
|
+
{ value: 'rollback', label: '回滚' },
|
|
2250
|
+
{ value: 'invariant_violated', label: '不变量违反' },
|
|
2251
|
+
{ value: 'assumption_falsified', label: '假设被证伪' },
|
|
2252
|
+
]
|
|
2253
|
+
|
|
2254
|
+
return React.createElement('div', {},
|
|
2255
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2256
|
+
React.createElement('b', {}, '防御事件'),
|
|
2257
|
+
React.createElement('button', {
|
|
2258
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'defense-list' ? '' : undefined,
|
|
2259
|
+
onClick: () => copyInstruction('defense-list', '请调用 `iterate_defense_events` 列出所有防御事件'),
|
|
2260
|
+
title: '复制列出防御事件指令',
|
|
2261
|
+
}, copiedKey === 'defense-list' ? '已复制' : '列出事件'),
|
|
2262
|
+
React.createElement('button', {
|
|
2263
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-counts' ? '' : undefined,
|
|
2264
|
+
onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
|
|
2265
|
+
title: '复制统计指令',
|
|
2266
|
+
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
2267
|
+
),
|
|
2268
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
2269
|
+
React.createElement('select', {
|
|
2270
|
+
className: 'iterate-filter-select',
|
|
2271
|
+
value: defenseFilter,
|
|
2272
|
+
'aria-label': '按事件类型筛选',
|
|
2273
|
+
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => setDefenseFilter(e.target.value),
|
|
2274
|
+
},
|
|
2275
|
+
...typeOptions.map((opt) => React.createElement('option', { key: opt.value, value: opt.value }, opt.label)),
|
|
2276
|
+
),
|
|
2277
|
+
defenseFilter
|
|
2278
|
+
? React.createElement('button', {
|
|
2279
|
+
className: 'iterate-filter-clear',
|
|
2280
|
+
onClick: () => setDefenseFilter(''),
|
|
2281
|
+
title: '清除类型筛选',
|
|
2282
|
+
}, '清除')
|
|
2283
|
+
: null,
|
|
2284
|
+
),
|
|
2285
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
2286
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2287
|
+
React.createElement('span', {}, '防御事件流'),
|
|
2288
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.0'),
|
|
2289
|
+
),
|
|
2290
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
2291
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, '防御事件包括:前置校验失败、回滚、不变量违反、假设被证伪。'),
|
|
2292
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
2293
|
+
React.createElement('button', {
|
|
2294
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'defense-instr' ? '' : undefined,
|
|
2295
|
+
onClick: () => copyInstruction('defense-instr', defenseInstruction),
|
|
2296
|
+
title: '复制查询指令',
|
|
2297
|
+
}, copiedKey === 'defense-instr' ? '已复制' : '复制查询指令'),
|
|
2298
|
+
),
|
|
2299
|
+
),
|
|
2300
|
+
),
|
|
2301
|
+
)
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2036
2304
|
// ── Live: real-time secondary-subagent activity stream (newest first) ─────
|
|
2037
2305
|
const renderLive = () => {
|
|
2038
2306
|
const liveEntries = manifest.live || []
|
|
@@ -2060,7 +2328,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2060
2328
|
const meta = OBS_LIVE_META[type] || { label: String(e.type || '?'), color: OBS_LIVE_FALLBACK_COLOR }
|
|
2061
2329
|
const tool = String(e.tool || '')
|
|
2062
2330
|
const target = String(e.target || '')
|
|
2063
|
-
const time = formatObsLiveTime(e.ts)
|
|
2331
|
+
const time = formatObsLiveTime(String(e.ts || ''))
|
|
2064
2332
|
return React.createElement('div', { key: `live-${i}`, className: 'iterate-obs-row' },
|
|
2065
2333
|
React.createElement('span', { className: 'iterate-obs-livebadge', style: { color: meta.color } }, meta.label),
|
|
2066
2334
|
tool
|
|
@@ -2109,6 +2377,10 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2109
2377
|
case 'f5': return renderCheckpoint()
|
|
2110
2378
|
case 'f6': return renderConsole()
|
|
2111
2379
|
case 'f7': return renderTimeline()
|
|
2380
|
+
// v3.0: Quality Command Center tabs
|
|
2381
|
+
case 'f8': return renderQualityGate()
|
|
2382
|
+
case 'f9': return renderExperienceBank()
|
|
2383
|
+
case 'f10': return renderDefenseEvents()
|
|
2112
2384
|
default: return null
|
|
2113
2385
|
}
|
|
2114
2386
|
}
|
package/src/config-loader.ts
CHANGED
|
@@ -207,6 +207,11 @@ export type ProjectRootResult = { ok: true; root: string } | { ok: false; reason
|
|
|
207
207
|
*/
|
|
208
208
|
export function resolveProjectRoot(input?: string, sessionCwd?: string): ProjectRootResult {
|
|
209
209
|
const raw = (input ?? '').trim()
|
|
210
|
+
// A NUL byte can never name a real path and makes `resolve()` (and every
|
|
211
|
+
// downstream fs call) throw — treat it as unsafe input, not a throw path.
|
|
212
|
+
if (raw.includes('\0')) {
|
|
213
|
+
return { ok: false, reason: 'Refusing project root containing NUL bytes.' }
|
|
214
|
+
}
|
|
210
215
|
const root = raw ? resolve(raw) : resolve(effectiveCwd(sessionCwd))
|
|
211
216
|
if (!root || root === sep) {
|
|
212
217
|
return { ok: false, reason: 'Refusing filesystem root as project root.' }
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
|
|
3
3
|
*
|
|
4
4
|
* Architecture:
|
|
5
|
-
* - The plugin registers
|
|
6
|
-
*
|
|
5
|
+
* - The plugin registers 17 tools (14 original + 3 v3.0 quality command center tools)
|
|
6
|
+
* Original: config, validate, decision-log, context, review, triage, fix, diff,
|
|
7
|
+
* rollback, checkpoint, status, history, prune, transcript
|
|
8
|
+
* v3.0: experience, quality_gate, defense_events
|
|
7
9
|
* - The plugin injects a system prompt section teaching the iterate workflow pattern
|
|
8
10
|
* - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
|
|
9
11
|
* - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
|
|
10
|
-
* - Subagents use the
|
|
11
|
-
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript
|
|
12
|
+
* - Subagents use the 17 tools to do real work (read config, run validation, log decisions,
|
|
13
|
+
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune, transcript,
|
|
14
|
+
* query experience bank, check quality gates, query defense events)
|
|
12
15
|
* - A `tools/pre-execute` hook gates destructive iterate calls behind human approval
|
|
13
16
|
* (F8 observatory approval policy: ask / deny / allow).
|
|
14
17
|
*
|
|
@@ -20,7 +23,7 @@
|
|
|
20
23
|
*
|
|
21
24
|
* Key files:
|
|
22
25
|
* - src/index.ts — Plugin entry: register tools + inject skill prompt
|
|
23
|
-
* - src/tools/ —
|
|
26
|
+
* - src/tools/ — 17 tool implementations (14 original + 3 v3.0)
|
|
24
27
|
* - src/config-loader.ts — YAML config loading
|
|
25
28
|
* - src/types.ts — Shared types
|
|
26
29
|
*/
|
|
@@ -37,6 +40,9 @@ import { registerCheckpointTool, registerStatusTool } from './tools/checkpoint.t
|
|
|
37
40
|
import { registerHistoryTool } from './tools/history.ts'
|
|
38
41
|
import { registerPruneTool } from './tools/prune.ts'
|
|
39
42
|
import { registerTranscriptTool } from './tools/transcript.ts'
|
|
43
|
+
import { registerExperienceBankTool } from './tools/experience-bank.ts'
|
|
44
|
+
import { registerQualityGateTool } from './tools/quality-gate.ts'
|
|
45
|
+
import { registerDefenseEventsTool } from './tools/defense-events.ts'
|
|
40
46
|
import { registerSessionHooks } from './session-hooks.ts'
|
|
41
47
|
import { registerLiveCapture } from './live.ts'
|
|
42
48
|
import { ITERATE_SKILL_PROMPT } from './skill-prompt.ts'
|
|
@@ -45,7 +51,7 @@ export const name = 'iterate-plugin'
|
|
|
45
51
|
export const inject = ['tools', 'systemPrompt'] as const
|
|
46
52
|
|
|
47
53
|
export function apply(ctx: Context): void {
|
|
48
|
-
// 1. Register the 14
|
|
54
|
+
// 1. Register the 17 tools (14 original + 3 v3.0)
|
|
49
55
|
registerConfigTool(ctx)
|
|
50
56
|
registerValidateTool(ctx)
|
|
51
57
|
registerDecisionLogTool(ctx)
|
|
@@ -60,6 +66,10 @@ export function apply(ctx: Context): void {
|
|
|
60
66
|
registerHistoryTool(ctx)
|
|
61
67
|
registerPruneTool(ctx)
|
|
62
68
|
registerTranscriptTool(ctx)
|
|
69
|
+
// v3.0: Quality Command Center tools
|
|
70
|
+
registerExperienceBankTool(ctx)
|
|
71
|
+
registerQualityGateTool(ctx)
|
|
72
|
+
registerDefenseEventsTool(ctx)
|
|
63
73
|
|
|
64
74
|
// 2. Wire the observatory approval gate onto dsh's tools/pre-execute waterfall,
|
|
65
75
|
// and the live reviewer-activity feed onto tools/result.
|
package/src/session-hooks.ts
CHANGED
|
@@ -36,17 +36,33 @@ import type { ToolExecution, PreToolDecision } from '@deepseek-ai/dsh-tools'
|
|
|
36
36
|
* Returns a dsh `PreToolDecision` so the caller can short-circuit the caller.
|
|
37
37
|
*/
|
|
38
38
|
export function gateDecision(exec: ToolExecution): PreToolDecision {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
39
|
+
// Defensively read the tool name: an exec handed to the waterfall is an
|
|
40
|
+
// ordinary object, but a hostile/proxied exec must degrade to "not our tool"
|
|
41
|
+
// (allow) instead of throwing before classification. The gate only ever
|
|
42
|
+
// inspects iterate tools, so an unreadable name also must not alter
|
|
43
|
+
// unrelated tooling.
|
|
44
|
+
let name = ''
|
|
45
|
+
try {
|
|
46
|
+
name = exec?.name ?? ''
|
|
47
|
+
} catch {
|
|
48
|
+
name = ''
|
|
49
|
+
}
|
|
50
|
+
if (!isDestructiveIterateTool(name)) return { kind: 'allow' }
|
|
42
51
|
|
|
43
52
|
// Resolve the project root (use the call's own `path` arg, else the agent's
|
|
44
53
|
// session cwd) to read the effective observatory policy.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
let argPath: string | undefined
|
|
55
|
+
let sessionCwd: string | undefined
|
|
56
|
+
try {
|
|
57
|
+
const args = exec?.arguments
|
|
58
|
+
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
59
|
+
const p = (args as Record<string, unknown>).path
|
|
60
|
+
if (typeof p === 'string') argPath = p
|
|
61
|
+
}
|
|
62
|
+
sessionCwd = exec?.agent?.session?.header?.cwd
|
|
63
|
+
} catch {
|
|
64
|
+
// hostile/proxied exec — fall through with both undefined (defaults to ask)
|
|
65
|
+
}
|
|
50
66
|
const resolved = resolveProjectRoot(argPath, sessionCwd)
|
|
51
67
|
let policy: 'ask' | 'deny' | 'allow' = 'ask'
|
|
52
68
|
if (resolved.ok) {
|
|
@@ -69,12 +85,18 @@ export function gateDecision(exec: ToolExecution): PreToolDecision {
|
|
|
69
85
|
*/
|
|
70
86
|
export function registerSessionHooks(ctx: Context): void {
|
|
71
87
|
ctx.on('tools/pre-execute', (exec: ToolExecution, next: () => Promise<PreToolDecision>) => {
|
|
72
|
-
//
|
|
88
|
+
// Fail-safe: a throwing gate must never fail OPEN. Degrade to `ask` so a
|
|
89
|
+
// destructive call still routes through human consent instead of running
|
|
90
|
+
// via `next()`'s allow default (matches the header's documented contract).
|
|
73
91
|
let decision: PreToolDecision
|
|
74
92
|
try {
|
|
75
93
|
decision = gateDecision(exec)
|
|
76
|
-
} catch {
|
|
77
|
-
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.warn('[iterate] approval gate failed; degrading to ask.', err)
|
|
96
|
+
return Promise.resolve({
|
|
97
|
+
kind: 'ask',
|
|
98
|
+
reason: 'iterate approval gate unavailable — require consent',
|
|
99
|
+
})
|
|
78
100
|
}
|
|
79
101
|
if (decision.kind === 'ask') {
|
|
80
102
|
// Delegate the actual human-consent prompt + audit to dsh's approval
|
package/src/skill-prompt.ts
CHANGED
|
@@ -24,6 +24,9 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
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
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.
|
|
27
|
+
- \`iterate_experience\` — experience bank (\`.iterate/experience.json\`): \`list\`/\`search\`/\`get\` recall verified fixes and patterns from past sessions (read the bank before fixing so proven fixes are applied first); \`add\` records a new verified fix — re-adding the same pattern+dimension bumps its hit count instead of duplicating it.
|
|
28
|
+
- \`iterate_quality_gate\` — quality certificate: \`read\` loads the persisted dimension convergence rates / verification pass rate / PASS-FAIL status; \`compute\` recomputes a fresh snapshot from this round's findings + validation results (supply \`findingsByRound\` for real convergence) and persists it to \`.iterate/quality-gate.json\`.
|
|
29
|
+
- \`iterate_defense_events\` — defense event stream (\`.iterate/defense-events.json\`): \`list\`/\`counts\` review precondition failures, rollbacks, invariant violations, and falsified assumptions; \`record\` logs a new event when a defense fires. Human-readable labels follow the project \`language\` (en/zh).
|
|
27
30
|
|
|
28
31
|
### When to use
|
|
29
32
|
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.
|
|
@@ -63,7 +63,7 @@ export function appendDecisionEntry(projectRoot: string, entry: DecisionLogEntry
|
|
|
63
63
|
const line = JSON.stringify(entry) + '\n'
|
|
64
64
|
appendFileSync(filePath, line, 'utf-8')
|
|
65
65
|
} catch (err) {
|
|
66
|
-
return { count:
|
|
66
|
+
return { count: 0, path: join(projectRoot, LOG_DIR, LOG_FILE), error: `failed to append decision log: ${String(err)}` }
|
|
67
67
|
}
|
|
68
68
|
// Count entries
|
|
69
69
|
let count = 0
|
|
@@ -215,6 +215,15 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
const result = appendDecisionEntry(projectRoot, entry)
|
|
218
|
+
if (result.error) {
|
|
219
|
+
return {
|
|
220
|
+
operation: 'append',
|
|
221
|
+
success: false,
|
|
222
|
+
entryCount: 0,
|
|
223
|
+
logPath: result.path,
|
|
224
|
+
error: result.error,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
218
227
|
return {
|
|
219
228
|
operation: 'append',
|
|
220
229
|
success: true,
|