iterate-plugin 3.11.0 → 3.13.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 +10 -9
- package/README.zh-CN.md +11 -10
- package/dist/index.js +8 -5
- package/dist/tools/config.js +109 -9
- package/dist/tools/preflight.js +280 -0
- package/dist/validation-detect.js +193 -0
- package/lib/client.js +352 -5
- package/lib/parse.js +172 -2
- package/package.json +5 -4
- package/src/client/i18n.ts +43 -1
- package/src/client/index.ts +221 -0
- package/src/index.ts +8 -5
- package/src/tools/config.ts +113 -10
- package/src/tools/preflight.ts +337 -0
- package/src/validation-detect.ts +207 -0
package/src/client/i18n.ts
CHANGED
|
@@ -446,11 +446,53 @@ export const EN: Record<string, string> = {
|
|
|
446
446
|
'防线:{a0}': 'Defense: {a0}',
|
|
447
447
|
没有匹配该类型的防御事件: 'No defense events of this type',
|
|
448
448
|
|
|
449
|
+
// ── F11 preflight ────────────────────────────────────────────────────────
|
|
450
|
+
开跑预检: 'Preflight',
|
|
451
|
+
待预检: 'Not run yet',
|
|
452
|
+
未就绪: 'Not ready',
|
|
453
|
+
'复制 iterate_preflight 预检指令(含 JSON 参数)':
|
|
454
|
+
'Copy the iterate_preflight readiness instruction (JSON arguments included)',
|
|
455
|
+
运行预检: 'Run preflight',
|
|
456
|
+
开跑预检清单: 'Preflight checklist',
|
|
457
|
+
预检结果: 'Preflight result',
|
|
458
|
+
'开跑前先预检:确认验证命令、Git、工作区、陈旧门禁与中断断点都已就绪,避免开跑后才发现环境问题。':
|
|
459
|
+
'Run preflight first: confirm validation commands, git, the working tree, a stale gate and an interrupted checkpoint are ready, so you do not discover an environment problem mid-run.',
|
|
460
|
+
复制预检指令: 'Copy preflight instruction',
|
|
461
|
+
验证命令: 'Validation commands',
|
|
462
|
+
'已配置 {a0} 条验证命令': '{a0} validation command(s) configured',
|
|
463
|
+
'未配置 validation.commands(阻断项)—— 检测到 {a0} 项目,建议:{a1}':
|
|
464
|
+
'validation.commands is not configured (blocking) — detected {a0} project; suggested: {a1}',
|
|
465
|
+
'未配置 validation.commands —— 本轮无法证明修复,属阻断项':
|
|
466
|
+
'validation.commands is not configured — the run cannot prove fixes (blocking)',
|
|
467
|
+
'Git 仓库': 'Git repository',
|
|
468
|
+
'已检测到 Git 仓库': 'Git repository detected',
|
|
469
|
+
'不是 Git 仓库 —— changed-only 范围与 iterate_diff 需要它':
|
|
470
|
+
'Not a git repository — changed-only scope and iterate_diff need one',
|
|
471
|
+
工作区: 'Working tree',
|
|
472
|
+
'未提交改动:未知': 'Uncommitted changes: unknown',
|
|
473
|
+
'{a0} 个未提交/未跟踪文件 —— 建议先 commit 或 stash':
|
|
474
|
+
'{a0} uncommitted/untracked file(s) — commit or stash first',
|
|
475
|
+
工作区干净: 'Working tree clean',
|
|
476
|
+
陈旧门禁: 'Stale gate',
|
|
477
|
+
'磁盘上残留 FAIL 质量门禁 —— 开新轮前先清除':
|
|
478
|
+
'A stale FAIL quality gate is persisted — clear it before a fresh run',
|
|
479
|
+
'质量门禁:{a0}': 'Quality gate: {a0}',
|
|
480
|
+
无持久化质量门禁: 'No persisted quality gate',
|
|
481
|
+
中断断点: 'Interrupted checkpoint',
|
|
482
|
+
'存在中断断点(已恢复 {a0} 次)—— 恢复或清除后再开跑':
|
|
483
|
+
'An interrupted checkpoint is waiting (resumed {a0}x) — resume or clear it first',
|
|
484
|
+
无中断断点: 'No interrupted checkpoint',
|
|
485
|
+
配置验证命令: 'Configure validation',
|
|
486
|
+
应用建议: 'Apply suggestion',
|
|
487
|
+
恢复断点: 'Resume checkpoint',
|
|
488
|
+
'阻断 {a0} · 警告 {a1}': 'Blocking {a0} · warnings {a1}',
|
|
489
|
+
'预检失败:{a0}': 'Preflight failed: {a0}',
|
|
490
|
+
'预检只读,不会修改配置或文件。': 'Preflight is read-only; it never modifies config or files.',
|
|
491
|
+
|
|
449
492
|
// ── copy feedback ────────────────────────────────────────────────────────
|
|
450
493
|
已复制: 'Copied',
|
|
451
494
|
复制失败: 'Copy failed',
|
|
452
495
|
}
|
|
453
|
-
|
|
454
496
|
let current: Lang = 'zh'
|
|
455
497
|
let storage: LangStorage | null = null
|
|
456
498
|
const listeners = new Set<(lang: Lang) => void>()
|
package/src/client/index.ts
CHANGED
|
@@ -75,6 +75,9 @@ import {
|
|
|
75
75
|
scanSessionForQualityGate,
|
|
76
76
|
scanSessionForExperienceBank,
|
|
77
77
|
scanSessionForDefenseEvents,
|
|
78
|
+
scanSessionForPreflight,
|
|
79
|
+
buildPreflightInstruction,
|
|
80
|
+
buildValidationApplyInstruction,
|
|
78
81
|
filterLiveEntries,
|
|
79
82
|
filterTimelineEntries,
|
|
80
83
|
serializeObservatoryExport,
|
|
@@ -336,6 +339,24 @@ interface ObsQualityGate {
|
|
|
336
339
|
dimensions?: ObsQualityGateDimension[]
|
|
337
340
|
}
|
|
338
341
|
|
|
342
|
+
/** One readiness check from `iterate_preflight` (F11). `detail` is structured
|
|
343
|
+
* facts the panel localizes into a sentence. */
|
|
344
|
+
interface ObsPreflightCheck {
|
|
345
|
+
id?: string
|
|
346
|
+
status?: string
|
|
347
|
+
detail?: Record<string, unknown>
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Latest `iterate_preflight` report scanned out of the session stream. */
|
|
351
|
+
interface ObsPreflight {
|
|
352
|
+
ok?: boolean
|
|
353
|
+
ready?: boolean
|
|
354
|
+
blocking?: number
|
|
355
|
+
warnings?: number
|
|
356
|
+
error?: string | null
|
|
357
|
+
checks?: ObsPreflightCheck[]
|
|
358
|
+
}
|
|
359
|
+
|
|
339
360
|
interface ObsExperienceEntry {
|
|
340
361
|
id?: string
|
|
341
362
|
timestamp?: string
|
|
@@ -583,6 +604,7 @@ const ITERATE_CSS = `
|
|
|
583
604
|
.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); }
|
|
584
605
|
.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); }
|
|
585
606
|
.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); }
|
|
607
|
+
.iterate-gate[data-status="warn"] { 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); }
|
|
586
608
|
|
|
587
609
|
/* v3.0: Experience bank hit highlight */
|
|
588
610
|
.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); }
|
|
@@ -2179,6 +2201,8 @@ const OBS_TABS = [
|
|
|
2179
2201
|
{ key: 'f8', label: { zh: '质量门禁', en: 'Quality gate' }, hotkey: '8' },
|
|
2180
2202
|
{ key: 'f9', label: { zh: '经验银行', en: 'Experience bank' }, hotkey: '9' },
|
|
2181
2203
|
{ key: 'f10', label: { zh: '防御事件', en: 'Defense events' }, hotkey: '0' },
|
|
2204
|
+
// v3.12: pre-run readiness checklist (validation/git/dirty-tree/stale-gate/checkpoint)
|
|
2205
|
+
{ key: 'f11', label: { zh: '开跑预检', en: 'Preflight' }, hotkey: '' },
|
|
2182
2206
|
] as const
|
|
2183
2207
|
|
|
2184
2208
|
/** Digit → tab id. `1-9` map to F1–F9, `0` to F10, `-` back to the live feed
|
|
@@ -2267,6 +2291,12 @@ function latestDefenseEvents(session: unknown): ObsDefenseResult | null {
|
|
|
2267
2291
|
return scanSessionForDefenseEvents(session) as ObsDefenseResult | null
|
|
2268
2292
|
}
|
|
2269
2293
|
|
|
2294
|
+
/** Find the latest preflight report inside a session snapshot. */
|
|
2295
|
+
function latestPreflight(session: unknown): ObsPreflight | null {
|
|
2296
|
+
if (!session) return null
|
|
2297
|
+
return scanSessionForPreflight(session) as ObsPreflight | null
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2270
2300
|
/** Build a human-pasteable `iterate_fix` instruction for one finding.
|
|
2271
2301
|
* Gap #10: delegates to the shared builder so the payload always names the
|
|
2272
2302
|
* four required `iterate_fix` params (file / content / finding / round) and
|
|
@@ -2295,6 +2325,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2295
2325
|
const qualityGate = latestQualityGate(session)
|
|
2296
2326
|
const experienceBank = latestExperienceBank(session)
|
|
2297
2327
|
const defenseEvents = latestDefenseEvents(session)
|
|
2328
|
+
const preflight = latestPreflight(session)
|
|
2298
2329
|
|
|
2299
2330
|
const [open, setOpen] = React.useState(false)
|
|
2300
2331
|
const [tab, setTab] = React.useState('live')
|
|
@@ -3356,6 +3387,195 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3356
3387
|
)
|
|
3357
3388
|
}
|
|
3358
3389
|
|
|
3390
|
+
// ── F11: Preflight readiness ──────────────────────────────────────────────
|
|
3391
|
+
// v3.12: renders the latest iterate_preflight checklist scanned from the
|
|
3392
|
+
// session stream. Each warn/fail row offers the exact fix instruction
|
|
3393
|
+
// (configure validation, clear a stale gate, resume an interrupted
|
|
3394
|
+
// checkpoint) so the environment is repaired BEFORE a round is spent on it.
|
|
3395
|
+
const renderPreflight = () => {
|
|
3396
|
+
const preflightInstruction = buildPreflightInstruction()
|
|
3397
|
+
const report = preflight
|
|
3398
|
+
const hasReport = report !== null && Array.isArray(report.checks)
|
|
3399
|
+
const ready = report !== null && report.ready === true
|
|
3400
|
+
const blocking = Number(report && report.blocking != null ? report.blocking : 0)
|
|
3401
|
+
const warnings = Number(report && report.warnings != null ? report.warnings : 0)
|
|
3402
|
+
const statusLabel = !hasReport ? t('待预检') : ready ? t('就绪') : t('未就绪')
|
|
3403
|
+
const statusTone = !hasReport ? 'pending' : ready ? 'pass' : 'fail'
|
|
3404
|
+
const headerBar = React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 8 } },
|
|
3405
|
+
React.createElement('b', {}, t('开跑预检')),
|
|
3406
|
+
React.createElement('span', { className: 'iterate-gate', 'data-status': statusTone }, statusLabel),
|
|
3407
|
+
React.createElement('button', {
|
|
3408
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'preflight' ? '' : undefined,
|
|
3409
|
+
onClick: () => copyInstruction('preflight', preflightInstruction),
|
|
3410
|
+
title: t('复制 iterate_preflight 预检指令(含 JSON 参数)'),
|
|
3411
|
+
}, copiedKey === 'preflight' ? t('已复制') : t('运行预检')),
|
|
3412
|
+
)
|
|
3413
|
+
|
|
3414
|
+
if (!hasReport) {
|
|
3415
|
+
return React.createElement('div', {},
|
|
3416
|
+
headerBar,
|
|
3417
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
3418
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3419
|
+
React.createElement('span', {}, t('开跑预检清单')),
|
|
3420
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.12'),
|
|
3421
|
+
),
|
|
3422
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3423
|
+
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3424
|
+
// Gap #4: separate "pulled and empty" from "never pulled".
|
|
3425
|
+
diskEmptyStateText(toolCalledInSession(session, 'iterate_preflight'), t('预检结果'), getLang()),
|
|
3426
|
+
),
|
|
3427
|
+
React.createElement('div', { className: 'iterate-obs-msg', style: { marginTop: 6 } },
|
|
3428
|
+
t('开跑前先预检:确认验证命令、Git、工作区、陈旧门禁与中断断点都已就绪,避免开跑后才发现环境问题。'),
|
|
3429
|
+
),
|
|
3430
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginTop: 8 } },
|
|
3431
|
+
React.createElement('button', {
|
|
3432
|
+
className: 'iterate-btn', 'data-copied': copiedKey === 'preflight-instr' ? '' : undefined,
|
|
3433
|
+
onClick: () => copyInstruction('preflight-instr', preflightInstruction),
|
|
3434
|
+
title: t('复制预检指令'),
|
|
3435
|
+
}, copiedKey === 'preflight-instr' ? t('已复制') : t('复制预检指令')),
|
|
3436
|
+
),
|
|
3437
|
+
),
|
|
3438
|
+
),
|
|
3439
|
+
)
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// A tool-level failure (bad path, root refused) carries no usable checks —
|
|
3443
|
+
// surface the error instead of a misleading "ready, 0 blocking" summary.
|
|
3444
|
+
if (report && report.ok === false) {
|
|
3445
|
+
return React.createElement('div', {},
|
|
3446
|
+
headerBar,
|
|
3447
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
3448
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3449
|
+
React.createElement('span', {}, t('开跑预检清单')),
|
|
3450
|
+
React.createElement('span', { className: 'iterate-obs-badge' }, 'v3.12'),
|
|
3451
|
+
),
|
|
3452
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3453
|
+
React.createElement('div', { className: 'iterate-obs-msg' },
|
|
3454
|
+
tf('预检失败:{a0}', { a0: String(report.error || '') }),
|
|
3455
|
+
),
|
|
3456
|
+
),
|
|
3457
|
+
),
|
|
3458
|
+
)
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// Localized label + detail per check id. Detail is structured facts from
|
|
3462
|
+
// the tool, so the panel — not the tool — owns the wording/language.
|
|
3463
|
+
const CHECK_META: Record<string, { label: string; detail: (d: Record<string, unknown>) => string }> = {
|
|
3464
|
+
validation: {
|
|
3465
|
+
label: t('验证命令'),
|
|
3466
|
+
detail: (d) => {
|
|
3467
|
+
const n = Number(d.commandCount ?? 0)
|
|
3468
|
+
if (n > 0) return tf('已配置 {a0} 条验证命令', { a0: n })
|
|
3469
|
+
const suggested = Array.isArray(d.suggestedCommands)
|
|
3470
|
+
? d.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0)
|
|
3471
|
+
: []
|
|
3472
|
+
if (suggested.length > 0) {
|
|
3473
|
+
return tf('未配置 validation.commands(阻断项)—— 检测到 {a0} 项目,建议:{a1}', {
|
|
3474
|
+
a0: String(d.ecosystem || ''),
|
|
3475
|
+
a1: suggested.join(' · '),
|
|
3476
|
+
})
|
|
3477
|
+
}
|
|
3478
|
+
return t('未配置 validation.commands —— 本轮无法证明修复,属阻断项')
|
|
3479
|
+
},
|
|
3480
|
+
},
|
|
3481
|
+
git: {
|
|
3482
|
+
label: t('Git 仓库'),
|
|
3483
|
+
detail: (d) => (d.isRepo === true ? t('已检测到 Git 仓库') : t('不是 Git 仓库 —— changed-only 范围与 iterate_diff 需要它')),
|
|
3484
|
+
},
|
|
3485
|
+
'dirty-tree': {
|
|
3486
|
+
label: t('工作区'),
|
|
3487
|
+
detail: (d) => {
|
|
3488
|
+
const n = d.changedFiles
|
|
3489
|
+
if (n === null || n === undefined) return t('未提交改动:未知')
|
|
3490
|
+
const num = Number(n)
|
|
3491
|
+
return num > 0
|
|
3492
|
+
? tf('{a0} 个未提交/未跟踪文件 —— 建议先 commit 或 stash', { a0: num })
|
|
3493
|
+
: t('工作区干净')
|
|
3494
|
+
},
|
|
3495
|
+
},
|
|
3496
|
+
'stale-gate': {
|
|
3497
|
+
label: t('陈旧门禁'),
|
|
3498
|
+
detail: (d) => {
|
|
3499
|
+
const s = d.overallStatus
|
|
3500
|
+
if (s === 'fail') return t('磁盘上残留 FAIL 质量门禁 —— 开新轮前先清除')
|
|
3501
|
+
return s ? tf('质量门禁:{a0}', { a0: String(s) }) : t('无持久化质量门禁')
|
|
3502
|
+
},
|
|
3503
|
+
},
|
|
3504
|
+
'interrupted-checkpoint': {
|
|
3505
|
+
label: t('中断断点'),
|
|
3506
|
+
detail: (d) => {
|
|
3507
|
+
if (d.interrupted === true) {
|
|
3508
|
+
const rc = Number(d.resumeCount ?? 0)
|
|
3509
|
+
return tf('存在中断断点(已恢复 {a0} 次)—— 恢复或清除后再开跑', { a0: rc })
|
|
3510
|
+
}
|
|
3511
|
+
return t('无中断断点')
|
|
3512
|
+
},
|
|
3513
|
+
},
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// Repair action per check id; null = guidance only (no tool can fix it).
|
|
3517
|
+
const fixFor = (id: string, detail: Record<string, unknown>): { key: string; label: string; instruction: string } | null => {
|
|
3518
|
+
switch (id) {
|
|
3519
|
+
case 'validation': {
|
|
3520
|
+
const suggested = Array.isArray(detail.suggestedCommands)
|
|
3521
|
+
? detail.suggestedCommands.map((c) => String(c)).filter((c) => c.length > 0)
|
|
3522
|
+
: []
|
|
3523
|
+
if (suggested.length > 0) {
|
|
3524
|
+
return {
|
|
3525
|
+
key: 'preflight-fix-validation',
|
|
3526
|
+
label: t('应用建议'),
|
|
3527
|
+
instruction: buildValidationApplyInstruction(suggested),
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
return { key: 'preflight-fix-validation', label: t('配置验证命令'), instruction: buildConfigFieldInstruction('validation.commands') }
|
|
3531
|
+
}
|
|
3532
|
+
case 'stale-gate':
|
|
3533
|
+
return { key: 'preflight-fix-gate', label: t('清除门禁'), instruction: buildQualityGateClearInstruction() }
|
|
3534
|
+
case 'interrupted-checkpoint':
|
|
3535
|
+
return { key: 'preflight-fix-checkpoint', label: t('恢复断点'), instruction: buildCheckpointResumeInstruction() }
|
|
3536
|
+
default:
|
|
3537
|
+
return null
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
const rows = (report.checks || []).map((c, i) => {
|
|
3542
|
+
const id = String(c.id || '')
|
|
3543
|
+
const status = String(c.status || 'warn')
|
|
3544
|
+
const meta = CHECK_META[id]
|
|
3545
|
+
const label = meta ? meta.label : id || '?'
|
|
3546
|
+
const detailText = meta ? meta.detail(c.detail || {}) : ''
|
|
3547
|
+
const fix = status !== 'pass' ? fixFor(id, c.detail || {}) : null
|
|
3548
|
+
return React.createElement('div', { key: `pf-${i}`, className: 'iterate-obs-row' },
|
|
3549
|
+
React.createElement('span', { className: 'iterate-gate', 'data-status': status }, status.toUpperCase()),
|
|
3550
|
+
React.createElement('b', {}, label),
|
|
3551
|
+
React.createElement('span', { className: 'iterate-obs-msg' }, detailText),
|
|
3552
|
+
fix
|
|
3553
|
+
? React.createElement('button', {
|
|
3554
|
+
className: 'iterate-btn', 'data-copied': copiedKey === fix.key ? '' : undefined,
|
|
3555
|
+
onClick: () => copyInstruction(fix.key, fix.instruction),
|
|
3556
|
+
title: fix.instruction,
|
|
3557
|
+
}, copiedKey === fix.key ? t('已复制') : fix.label)
|
|
3558
|
+
: null,
|
|
3559
|
+
)
|
|
3560
|
+
})
|
|
3561
|
+
|
|
3562
|
+
return React.createElement('div', {},
|
|
3563
|
+
headerBar,
|
|
3564
|
+
React.createElement('div', { className: 'iterate-obs-block' },
|
|
3565
|
+
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
3566
|
+
React.createElement('span', {}, t('开跑预检清单')),
|
|
3567
|
+
React.createElement('span', { className: 'iterate-obs-head-meta' },
|
|
3568
|
+
tf('阻断 {a0} · 警告 {a1}', { a0: blocking, a1: warnings }),
|
|
3569
|
+
),
|
|
3570
|
+
),
|
|
3571
|
+
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
3572
|
+
React.createElement('div', { className: 'iterate-obs-msg' }, t('预检只读,不会修改配置或文件。')),
|
|
3573
|
+
...rows,
|
|
3574
|
+
),
|
|
3575
|
+
),
|
|
3576
|
+
)
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3359
3579
|
// ── Live: real-time secondary-subagent activity stream (newest first) ─────
|
|
3360
3580
|
const renderLive = () => {
|
|
3361
3581
|
const liveEntries = manifest.live || []
|
|
@@ -3440,6 +3660,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
3440
3660
|
case 'f8': return renderQualityGate()
|
|
3441
3661
|
case 'f9': return renderExperienceBank()
|
|
3442
3662
|
case 'f10': return renderDefenseEvents()
|
|
3663
|
+
case 'f11': return renderPreflight()
|
|
3443
3664
|
default: return null
|
|
3444
3665
|
}
|
|
3445
3666
|
}
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { registerExperienceBankTool } from './tools/experience-bank.ts'
|
|
|
46
46
|
import { registerQualityGateTool } from './tools/quality-gate.ts'
|
|
47
47
|
import { registerDefenseEventsTool } from './tools/defense-events.ts'
|
|
48
48
|
import { registerReportTool } from './tools/report.ts'
|
|
49
|
+
import { registerPreflightTool } from './tools/preflight.ts'
|
|
49
50
|
import { registerSessionHooks } from './session-hooks.ts'
|
|
50
51
|
import { registerLiveCapture } from './live.ts'
|
|
51
52
|
import { installWorkingDirectoryContext } from './working-directory.ts'
|
|
@@ -71,11 +72,11 @@ export function apply(ctx: Context): void {
|
|
|
71
72
|
registerSessionHooks(ctx)
|
|
72
73
|
registerLiveCapture(ctx)
|
|
73
74
|
|
|
74
|
-
// 1. Register the
|
|
75
|
-
// isolated: one failing tool must not take down the other
|
|
76
|
-
// above, or the prompt below (a partial plugin that still
|
|
77
|
-
// teaches the workflow beats an aborted `apply`). Failures are
|
|
78
|
-
// never swallowed silently.
|
|
75
|
+
// 1. Register the 19 tools (14 original + 3 v3.0 + report + preflight). Each
|
|
76
|
+
// registration is isolated: one failing tool must not take down the other
|
|
77
|
+
// 18, the gate above, or the prompt below (a partial plugin that still
|
|
78
|
+
// gates and teaches the workflow beats an aborted `apply`). Failures are
|
|
79
|
+
// logged, never swallowed silently.
|
|
79
80
|
const toolRegistrations: Array<[string, () => void]> = [
|
|
80
81
|
['iterate_config', () => registerConfigTool(ctx)],
|
|
81
82
|
['iterate_validate', () => registerValidateTool(ctx)],
|
|
@@ -97,6 +98,8 @@ export function apply(ctx: Context): void {
|
|
|
97
98
|
['iterate_defense_events', () => registerDefenseEventsTool(ctx)],
|
|
98
99
|
// v3.9: PR-ready / CI-ready report export
|
|
99
100
|
['iterate_report', () => registerReportTool(ctx)],
|
|
101
|
+
// v3.12: pre-run readiness check (validation/git/stale-gate/interrupted)
|
|
102
|
+
['iterate_preflight', () => registerPreflightTool(ctx)],
|
|
100
103
|
]
|
|
101
104
|
for (const [toolName, register] of toolRegistrations) {
|
|
102
105
|
try {
|
package/src/tools/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from 'node:path'
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
4
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
5
|
-
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from '../config-loader.ts'
|
|
5
|
+
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec, flattenCommands } from '../config-loader.ts'
|
|
6
6
|
import {
|
|
7
7
|
applyConfigUpdates,
|
|
8
8
|
readRawConfig,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
writeConfigFile,
|
|
11
11
|
} from '../config-write.ts'
|
|
12
12
|
import { withProjectLock } from '../file-lock.ts'
|
|
13
|
+
import { buildValidationUpdates, detectValidationCommands } from '../validation-detect.ts'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Register the `iterate_config` tool.
|
|
@@ -21,16 +22,24 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
21
22
|
ctx.tools.register(
|
|
22
23
|
defineTool({
|
|
23
24
|
name: 'iterate_config',
|
|
24
|
-
// `read` never writes; `write` persists a config update
|
|
25
|
-
//
|
|
26
|
-
isConcurrencySafe: (args) =>
|
|
25
|
+
// `read` never writes; `write` persists a config update. `detect` is a
|
|
26
|
+
// read UNLESS it is asked to apply the detected commands.
|
|
27
|
+
isConcurrencySafe: (args) => {
|
|
28
|
+
const a = args as { operation?: unknown; apply?: unknown }
|
|
29
|
+
return a.operation !== 'write' && !(a.operation === 'detect' && a.apply === true)
|
|
30
|
+
},
|
|
27
31
|
// Pending-call card: a config read vs a validated write.
|
|
28
32
|
presentCall: (args) => {
|
|
29
|
-
const a = args as { operation?: unknown; section?: unknown }
|
|
30
|
-
const isWrite = a.operation === 'write'
|
|
33
|
+
const a = args as { operation?: unknown; section?: unknown; apply?: unknown }
|
|
34
|
+
const isWrite = a.operation === 'write' || (a.operation === 'detect' && a.apply === true)
|
|
35
|
+
const title = isWrite
|
|
36
|
+
? 'Update iterate.config.yaml'
|
|
37
|
+
: a.operation === 'detect'
|
|
38
|
+
? 'Detect validation commands'
|
|
39
|
+
: 'Read iterate.config.yaml'
|
|
31
40
|
return {
|
|
32
41
|
card: 'generic',
|
|
33
|
-
title
|
|
42
|
+
title,
|
|
34
43
|
kind: isWrite ? 'edit' : 'read',
|
|
35
44
|
...(typeof a.section === 'string' && a.section ? { rawInput: { section: a.section } } : {}),
|
|
36
45
|
}
|
|
@@ -39,13 +48,17 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
39
48
|
'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
40
49
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
|
41
50
|
'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
|
|
42
|
-
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.).'
|
|
51
|
+
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.). ' +
|
|
52
|
+
'operation "detect" inspects the project manifest (package.json scripts / pytest / Cargo / Go / Make) and ' +
|
|
53
|
+
'proposes a ready-to-apply validation.commands set; pass apply:true to install it in one call.',
|
|
43
54
|
|
|
44
55
|
parameters: {
|
|
45
56
|
operation: {
|
|
46
57
|
type: 'string',
|
|
47
|
-
description:
|
|
48
|
-
|
|
58
|
+
description:
|
|
59
|
+
'Default "read". "write" validates and applies a partial config update (backed up first). ' +
|
|
60
|
+
'"detect" proposes validation commands from the project manifest.',
|
|
61
|
+
enum: ['read', 'write', 'detect'],
|
|
49
62
|
},
|
|
50
63
|
path: {
|
|
51
64
|
type: 'string',
|
|
@@ -60,6 +73,16 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
60
73
|
type: 'boolean',
|
|
61
74
|
description: 'If true, validate the config schema and return any missing fields.',
|
|
62
75
|
},
|
|
76
|
+
apply: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
description:
|
|
79
|
+
'For operation "detect": when true, install the detected commands under validation.commands.default ' +
|
|
80
|
+
'(refused if commands are already configured unless force:true).',
|
|
81
|
+
},
|
|
82
|
+
force: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'For operation "detect" with apply:true: replace an existing default command set.',
|
|
85
|
+
},
|
|
63
86
|
updates: {
|
|
64
87
|
type: 'json',
|
|
65
88
|
description:
|
|
@@ -83,6 +106,11 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
83
106
|
availableSections: { type: 'array', items: { type: 'string' } },
|
|
84
107
|
operation: { type: 'string' },
|
|
85
108
|
ok: { type: 'boolean' },
|
|
109
|
+
applied: { type: 'boolean' },
|
|
110
|
+
ecosystem: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
111
|
+
commands: { type: 'array', items: { type: 'string' } },
|
|
112
|
+
sources: { type: 'json' },
|
|
113
|
+
updates: { type: 'json' },
|
|
86
114
|
backupPath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
87
115
|
error: { type: 'string' },
|
|
88
116
|
},
|
|
@@ -99,6 +127,81 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
99
127
|
}
|
|
100
128
|
const projectRoot = resolved.root
|
|
101
129
|
|
|
130
|
+
// ── Detect operation (validation-command auto-detection) ───────────
|
|
131
|
+
// Reads the project manifest and proposes a curated command set. With
|
|
132
|
+
// apply:true it installs the set under validation.commands.default in
|
|
133
|
+
// the same validated, backed-up, locked write path as operation:write.
|
|
134
|
+
if (args.operation === 'detect') {
|
|
135
|
+
const configPath = join(projectRoot, 'iterate.config.yaml')
|
|
136
|
+
const configExists = existsSync(configPath)
|
|
137
|
+
const detection = detectValidationCommands(projectRoot)
|
|
138
|
+
const updates = buildValidationUpdates(detection.commands)
|
|
139
|
+
const base = {
|
|
140
|
+
operation: 'detect',
|
|
141
|
+
found: configExists,
|
|
142
|
+
ecosystem: detection.ecosystem,
|
|
143
|
+
commands: detection.commands,
|
|
144
|
+
sources: detection.sources as unknown as JsonValue,
|
|
145
|
+
updates: updates as unknown as JsonValue,
|
|
146
|
+
}
|
|
147
|
+
if (detection.commands.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
ok: false,
|
|
151
|
+
applied: false,
|
|
152
|
+
error:
|
|
153
|
+
'no validation commands detected — the project does not advertise a test/typecheck/lint/build ' +
|
|
154
|
+
'command; configure validation.commands manually',
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (args.apply !== true) {
|
|
158
|
+
return { ...base, ok: true, applied: false }
|
|
159
|
+
}
|
|
160
|
+
// Refuse to silently clobber a hand-tuned command set.
|
|
161
|
+
const current = loadEffectiveConfig(projectRoot).config
|
|
162
|
+
const existing = flattenCommands(current.validation?.commands)
|
|
163
|
+
if (existing.length > 0 && args.force !== true) {
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
ok: false,
|
|
167
|
+
applied: false,
|
|
168
|
+
error:
|
|
169
|
+
`validation.commands already has ${existing.length} command(s); ` +
|
|
170
|
+
'pass force:true to replace the "default" module',
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return withProjectLock(projectRoot, 'config-write', () => {
|
|
174
|
+
let cfgBase: Record<string, unknown>
|
|
175
|
+
try {
|
|
176
|
+
cfgBase = configExists
|
|
177
|
+
? readRawConfig(configPath)
|
|
178
|
+
: (defaultConfig() as unknown as Record<string, unknown>)
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return { ...base, ok: false, applied: false, error: `failed to read config: ${String(err)}` }
|
|
181
|
+
}
|
|
182
|
+
const merged = applyConfigUpdates(cfgBase, updates)
|
|
183
|
+
const schemaErrors = validateConfig(merged)
|
|
184
|
+
if (schemaErrors.length > 0) {
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
ok: false,
|
|
188
|
+
applied: false,
|
|
189
|
+
errors: schemaErrors.map((e) => `missing/required field: ${e}`),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const result = writeConfigFile(projectRoot, merged)
|
|
193
|
+
if (!result.ok) return { ...base, ok: false, applied: false, error: result.error }
|
|
194
|
+
const { config } = loadEffectiveConfig(projectRoot)
|
|
195
|
+
return {
|
|
196
|
+
...base,
|
|
197
|
+
ok: true,
|
|
198
|
+
applied: true,
|
|
199
|
+
backupPath: result.backupPath ?? null,
|
|
200
|
+
config: config as unknown as JsonValue,
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
102
205
|
// ── Write operation ────────────────────────────────────────────────
|
|
103
206
|
if (args.operation === 'write') {
|
|
104
207
|
const updates = args.updates as Record<string, unknown> | undefined
|