@zhuan-ai/zhuanspec 2.15.7 → 2.16.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/dist/cli/hooks.d.ts +13 -1
- package/dist/cli/hooks.js +75 -2
- package/dist/cli/index.js +94 -4
- package/dist/commands/accuracy.js +5 -1
- package/dist/commands/knowledge.d.ts +21 -0
- package/dist/commands/knowledge.js +80 -0
- package/dist/commands/progress.d.ts +66 -0
- package/dist/commands/progress.js +306 -1
- package/dist/core/archive.js +28 -0
- package/dist/core/configurators/codex.d.ts +55 -1
- package/dist/core/configurators/codex.js +234 -2
- package/dist/core/hooks/collect-knowledge.js +66 -0
- package/dist/core/hooks/deviation-check.d.ts +26 -1
- package/dist/core/hooks/deviation-check.js +32 -184
- package/dist/core/hooks/knowledge-index.d.ts +84 -0
- package/dist/core/hooks/knowledge-index.js +270 -0
- package/dist/core/hooks/record-progress.d.ts +63 -1
- package/dist/core/hooks/record-progress.js +208 -54
- package/dist/core/hooks/summarize.js +90 -2
- package/dist/core/hooks/user-input-hook.d.ts +38 -2
- package/dist/core/hooks/user-input-hook.js +346 -21
- package/dist/core/init.d.ts +11 -0
- package/dist/core/init.js +43 -4
- package/dist/core/metrics/code-accuracy.d.ts +152 -3
- package/dist/core/metrics/code-accuracy.js +323 -19
- package/dist/core/templates/agents-root-stub.d.ts +1 -1
- package/dist/core/templates/agents-root-stub.js +1 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +21 -4
- package/dist/core/templates/codex-hooks-template.d.ts +37 -0
- package/dist/core/templates/codex-hooks-template.js +33 -2
- package/dist/core/templates/slash-command-templates.js +144 -28
- package/dist/core/templates/tasks-template.js +66 -14
- package/dist/core/update.js +17 -0
- package/dist/core/validation/strict-rules.d.ts +28 -0
- package/dist/core/validation/strict-rules.js +284 -0
- package/dist/utils/hook-merge.d.ts +6 -0
- package/dist/utils/hook-merge.js +33 -2
- package/package.json +1 -1
|
@@ -13,12 +13,22 @@
|
|
|
13
13
|
*
|
|
14
14
|
* ~/.codex/ (or $CODEX_HOME)
|
|
15
15
|
* config.toml - Per-project [projects."…"] trusted_level = "trusted"
|
|
16
|
-
* block (wrapped in TRUST markers keyed by absolute path)
|
|
16
|
+
* block (wrapped in TRUST markers keyed by absolute path).
|
|
17
|
+
* Also receives [hooks.state."<abs>:<event>:<i>:<j>"]
|
|
18
|
+
* entries grafted from other already-trusted projects,
|
|
19
|
+
* wrapped in HOOK_STATE markers keyed by config.toml path.
|
|
17
20
|
*
|
|
18
21
|
* Codex does **not** load project-scoped hooks/subagents unless the project
|
|
19
22
|
* is explicitly marked trusted in the global config (see openai/codex#14601),
|
|
20
23
|
* so we opportunistically write that trust entry during `zhuanspec init`.
|
|
21
24
|
*
|
|
25
|
+
* Codex CLI >= 0.129 additionally requires each hook's content hash to be
|
|
26
|
+
* pre-approved (openai/codex#20321) or the user sees "N hooks need review".
|
|
27
|
+
* The `trusted_hash` in `[hooks.state.<key>]` depends only on normalised hook
|
|
28
|
+
* *content* (event + matcher + command), not on the key path, so we clone a
|
|
29
|
+
* previously approved hash to the current project's key to skip the manual
|
|
30
|
+
* review prompt in fresh workspaces.
|
|
31
|
+
*
|
|
22
32
|
* Idempotency:
|
|
23
33
|
* - Hooks block is regenerated inside ZHUANSPEC markers on every run.
|
|
24
34
|
* - Trust entries are keyed by absolute path; repeated runs no-op.
|
|
@@ -28,7 +38,7 @@ import path from 'path';
|
|
|
28
38
|
import os from 'os';
|
|
29
39
|
import { promises as fs } from 'fs';
|
|
30
40
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
31
|
-
import { getCodexHooksTomlBlock } from '../templates/codex-hooks-template.js';
|
|
41
|
+
import { getCodexHooksTomlBlock, getCodexDefaultHookSlots } from '../templates/codex-hooks-template.js';
|
|
32
42
|
import { getCodexAgentTomls, } from '../templates/codex-agents-template.js';
|
|
33
43
|
import { getCodexSkillFiles } from '../templates/codex-skills-template.js';
|
|
34
44
|
import { CodexSlashCommandConfigurator } from './slash/codex.js';
|
|
@@ -51,6 +61,18 @@ function buildTrustMarkers(projectPath) {
|
|
|
51
61
|
end: `# <!-- ZHUANSPEC:TRUST:END path=${projectPath} -->`,
|
|
52
62
|
};
|
|
53
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the per-path HOOK_STATE marker pair for the global config.toml.
|
|
66
|
+
* Keyed by the **project config.toml absolute path** (not the project dir)
|
|
67
|
+
* so it aligns with the prefix of each `[hooks.state."<config-path>:..."]`
|
|
68
|
+
* key that Codex records.
|
|
69
|
+
*/
|
|
70
|
+
function buildHookStateMarkers(projectConfigTomlPath) {
|
|
71
|
+
return {
|
|
72
|
+
start: `# <!-- ZHUANSPEC:HOOK_STATE:START path=${projectConfigTomlPath} -->`,
|
|
73
|
+
end: `# <!-- ZHUANSPEC:HOOK_STATE:END path=${projectConfigTomlPath} -->`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
54
76
|
export class CodexConfigurator {
|
|
55
77
|
name = 'Codex';
|
|
56
78
|
/** Codex reads AGENTS.md for project instructions (no dedicated CODEX.md). */
|
|
@@ -80,6 +102,7 @@ export class CodexConfigurator {
|
|
|
80
102
|
const skillsWritten = await this.mirrorSkills(projectPath);
|
|
81
103
|
const configTomlPath = await this.writeProjectConfigToml(codexDir, agentFiles);
|
|
82
104
|
const { globalConfigPath, trustEntryAdded } = await this.ensureGlobalTrustEntry(projectPath);
|
|
105
|
+
const hookTrust = await this.ensureHookTrustSeed(globalConfigPath, configTomlPath);
|
|
83
106
|
const conflictingGlobalHooksJson = await this.detectGlobalHooksJsonConflict();
|
|
84
107
|
// Codex CLI >= 0.117 no longer scans `~/.codex/prompts/`; delete any
|
|
85
108
|
// ZhuanSpec-managed prompt left behind by pre-2.12.5 installs so the
|
|
@@ -94,6 +117,7 @@ export class CodexConfigurator {
|
|
|
94
117
|
trustEntryAdded,
|
|
95
118
|
conflictingGlobalHooksJson,
|
|
96
119
|
legacyPromptsCleaned,
|
|
120
|
+
hookTrust,
|
|
97
121
|
};
|
|
98
122
|
}
|
|
99
123
|
/**
|
|
@@ -292,5 +316,213 @@ export class CodexConfigurator {
|
|
|
292
316
|
const block = this.buildTrustBlock(projectPath);
|
|
293
317
|
return `${before}${block}${after}`;
|
|
294
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Graft `[hooks.state."<key>"] trusted_hash = "sha256:..."` entries for the
|
|
321
|
+
* current project from any other project that the user has already approved
|
|
322
|
+
* via Codex's `/hooks` TUI. The grafted block is wrapped in HOOK_STATE
|
|
323
|
+
* markers keyed by `configTomlPath` so it stays idempotent across runs and
|
|
324
|
+
* coexists with HOOK_STATE blocks for sibling projects in the same file.
|
|
325
|
+
*
|
|
326
|
+
* Strategy:
|
|
327
|
+
* - Strip our own existing block first to avoid self-referencing seeds.
|
|
328
|
+
* - Scan remaining `[hooks.state."<path>:<event>:<grp>:<hdl>"]` tables;
|
|
329
|
+
* index their `trusted_hash` by the `<event>:<grp>:<hdl>` suffix so the
|
|
330
|
+
* same logical hook in any other project can serve as a seed.
|
|
331
|
+
* - For each ZhuanSpec default slot:
|
|
332
|
+
* * If a native (unmanaged) entry already owns the same key in this
|
|
333
|
+
* project, skip to avoid TOML duplicate-key errors.
|
|
334
|
+
* * Else if a seed exists, emit a managed entry pointing the user's
|
|
335
|
+
* project key at the known-good hash.
|
|
336
|
+
* * Else record the slot in `skippedNoSeed` so callers can surface a
|
|
337
|
+
* "please approve once in Codex" hint.
|
|
338
|
+
*
|
|
339
|
+
* Failures are non-fatal: any unexpected error is swallowed and reported
|
|
340
|
+
* via an empty result with `seedAvailable=false`, since hook trust is a
|
|
341
|
+
* UX optimisation — missing it just means the user sees the regular
|
|
342
|
+
* Codex review prompt.
|
|
343
|
+
*/
|
|
344
|
+
async ensureHookTrustSeed(globalConfigPath, configTomlPath) {
|
|
345
|
+
const slots = getCodexDefaultHookSlots();
|
|
346
|
+
const empty = {
|
|
347
|
+
written: [],
|
|
348
|
+
skippedNoSeed: slots.map(slotSuffix),
|
|
349
|
+
skippedNative: [],
|
|
350
|
+
seedAvailable: false,
|
|
351
|
+
};
|
|
352
|
+
try {
|
|
353
|
+
let existing = '';
|
|
354
|
+
if (await FileSystemUtils.fileExists(globalConfigPath)) {
|
|
355
|
+
existing = await FileSystemUtils.readFile(globalConfigPath);
|
|
356
|
+
}
|
|
357
|
+
// 1. Strip our own block so we don't seed from ourselves and so the
|
|
358
|
+
// "native conflict" check only sees user/legacy entries.
|
|
359
|
+
const stripped = stripHookStateBlock(existing, configTomlPath);
|
|
360
|
+
// 2. Build seed map (suffix → first hash seen anywhere except this project).
|
|
361
|
+
const allEntries = parseHookStateEntries(stripped);
|
|
362
|
+
const seedBySuffix = new Map();
|
|
363
|
+
const nativeOwnedSuffixes = new Set();
|
|
364
|
+
for (const entry of allEntries) {
|
|
365
|
+
const split = splitHookStateKey(entry.key);
|
|
366
|
+
if (!split)
|
|
367
|
+
continue;
|
|
368
|
+
if (split.pathPart === configTomlPath) {
|
|
369
|
+
// Native (unmanaged) entry already exists for this project's slot.
|
|
370
|
+
nativeOwnedSuffixes.add(split.suffix);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!seedBySuffix.has(split.suffix)) {
|
|
374
|
+
seedBySuffix.set(split.suffix, entry.hash);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const seedAvailable = seedBySuffix.size > 0;
|
|
378
|
+
// 3. Plan per-slot writes.
|
|
379
|
+
const written = [];
|
|
380
|
+
const skippedNoSeed = [];
|
|
381
|
+
const skippedNative = [];
|
|
382
|
+
const blockEntries = [];
|
|
383
|
+
for (const slot of slots) {
|
|
384
|
+
const suffix = slotSuffix(slot);
|
|
385
|
+
if (nativeOwnedSuffixes.has(suffix)) {
|
|
386
|
+
skippedNative.push(suffix);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const seed = seedBySuffix.get(suffix);
|
|
390
|
+
if (!seed) {
|
|
391
|
+
skippedNoSeed.push(suffix);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const fullKey = `${configTomlPath}:${suffix}`;
|
|
395
|
+
blockEntries.push({ key: fullKey, hash: seed });
|
|
396
|
+
written.push(suffix);
|
|
397
|
+
}
|
|
398
|
+
// 4. Persist. When nothing to write, ensure any stale managed block is
|
|
399
|
+
// removed (e.g. user revoked all hooks elsewhere); otherwise replace.
|
|
400
|
+
const finalContent = blockEntries.length === 0
|
|
401
|
+
? stripped
|
|
402
|
+
: insertHookStateBlock(stripped, configTomlPath, buildHookStateBlockBody(configTomlPath, blockEntries));
|
|
403
|
+
if (finalContent !== existing) {
|
|
404
|
+
await FileSystemUtils.writeFile(globalConfigPath, finalContent);
|
|
405
|
+
}
|
|
406
|
+
return { written, skippedNoSeed, skippedNative, seedAvailable };
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Non-fatal: hook trust seeding is a UX optimisation.
|
|
410
|
+
return empty;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Hook trust seed helpers (module-private).
|
|
416
|
+
//
|
|
417
|
+
// These intentionally avoid pulling in a TOML parser dependency — we only
|
|
418
|
+
// need to read/write a tightly scoped subset of `~/.codex/config.toml`
|
|
419
|
+
// (`[hooks.state."..."]` tables with a single `trusted_hash` value) and
|
|
420
|
+
// must round-trip the rest of the user's file byte-for-byte.
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
/** `<event>:<group>:<handler>` suffix used both for seed lookup and key building. */
|
|
423
|
+
function slotSuffix(slot) {
|
|
424
|
+
return `${slot.eventSnake}:${slot.groupIndex}:${slot.handlerIndex}`;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Parse `[hooks.state."<key>"] trusted_hash = "..."` entries from a TOML
|
|
428
|
+
* blob using a line walker (no full TOML grammar needed).
|
|
429
|
+
*/
|
|
430
|
+
function parseHookStateEntries(content) {
|
|
431
|
+
const lines = content.split('\n');
|
|
432
|
+
const entries = [];
|
|
433
|
+
const headerRe = /^\[hooks\.state\."((?:[^"\\]|\\.)+)"\]\s*$/;
|
|
434
|
+
const hashRe = /^trusted_hash\s*=\s*"([^"]+)"\s*$/;
|
|
435
|
+
for (let i = 0; i < lines.length; i++) {
|
|
436
|
+
const m = lines[i].match(headerRe);
|
|
437
|
+
if (!m)
|
|
438
|
+
continue;
|
|
439
|
+
const rawKey = m[1];
|
|
440
|
+
// Walk forward until the next table header or EOF, capturing trusted_hash.
|
|
441
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
442
|
+
const trimmed = lines[j].trim();
|
|
443
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
444
|
+
continue;
|
|
445
|
+
if (trimmed.startsWith('['))
|
|
446
|
+
break;
|
|
447
|
+
const h = trimmed.match(hashRe);
|
|
448
|
+
if (h) {
|
|
449
|
+
entries.push({ key: unescapeTomlBasic(rawKey), hash: h[1] });
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return entries;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Split a hook-state key (`<config-toml-path>:<event>:<grp>:<hdl>`) into its
|
|
458
|
+
* path prefix and 3-field suffix. Handles paths containing `:` (Windows
|
|
459
|
+
* drives, etc.) by splitting on the **last** three colons.
|
|
460
|
+
*/
|
|
461
|
+
function splitHookStateKey(key) {
|
|
462
|
+
const parts = key.split(':');
|
|
463
|
+
if (parts.length < 4)
|
|
464
|
+
return null;
|
|
465
|
+
const suffix = parts.slice(-3).join(':');
|
|
466
|
+
const pathPart = parts.slice(0, parts.length - 3).join(':');
|
|
467
|
+
if (!pathPart)
|
|
468
|
+
return null;
|
|
469
|
+
return { pathPart, suffix };
|
|
470
|
+
}
|
|
471
|
+
/** Reverse TOML basic-string escapes for `\\\\` and `\\"` (sufficient for paths). */
|
|
472
|
+
function unescapeTomlBasic(raw) {
|
|
473
|
+
return raw.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
|
|
474
|
+
}
|
|
475
|
+
/** Forward TOML basic-string escape — mirrors `escapeBasic` used elsewhere. */
|
|
476
|
+
function escapeTomlBasic(raw) {
|
|
477
|
+
return raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
478
|
+
}
|
|
479
|
+
/** Build the body (between markers) of the HOOK_STATE managed block. */
|
|
480
|
+
function buildHookStateBlockBody(configTomlPath, entries) {
|
|
481
|
+
const { start, end } = buildHookStateMarkers(configTomlPath);
|
|
482
|
+
const tables = entries.map((e) => [`[hooks.state."${escapeTomlBasic(e.key)}"]`, `trusted_hash = "${e.hash}"`].join('\n'));
|
|
483
|
+
const header = [
|
|
484
|
+
'# ZhuanSpec hook trust seeds — grafted from previously approved projects.',
|
|
485
|
+
'# Managed block; regenerated by `zhuanspec init/update`. Do not edit by hand.',
|
|
486
|
+
].join('\n');
|
|
487
|
+
return [start, header, '', tables.join('\n\n'), end].join('\n');
|
|
488
|
+
}
|
|
489
|
+
/** Remove our marker-wrapped HOOK_STATE block for `configTomlPath` from `content`. */
|
|
490
|
+
function stripHookStateBlock(content, configTomlPath) {
|
|
491
|
+
const { start, end } = buildHookStateMarkers(configTomlPath);
|
|
492
|
+
const startIdx = content.indexOf(start);
|
|
493
|
+
if (startIdx === -1)
|
|
494
|
+
return content;
|
|
495
|
+
const endIdx = content.indexOf(end, startIdx + start.length);
|
|
496
|
+
if (endIdx === -1)
|
|
497
|
+
return content;
|
|
498
|
+
const blockStart = startIdx;
|
|
499
|
+
let blockEnd = endIdx + end.length;
|
|
500
|
+
// Consume the trailing newline written together with the block so we
|
|
501
|
+
// don't leave an orphan blank line behind. Leave any leading newline
|
|
502
|
+
// intact so `insertHookStateBlock` produces a stable, idempotent layout.
|
|
503
|
+
if (content[blockEnd] === '\n')
|
|
504
|
+
blockEnd += 1;
|
|
505
|
+
return content.substring(0, blockStart) + content.substring(blockEnd);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Insert (or replace) the HOOK_STATE managed block for `configTomlPath` in
|
|
509
|
+
* `content`. When a previous block exists, it's swapped in-place to keep the
|
|
510
|
+
* file order stable. Otherwise the block is appended to EOF; we adjust the
|
|
511
|
+
* separator so a follow-up `strip + insert` round-trip produces byte-for-byte
|
|
512
|
+
* the same result (idempotency).
|
|
513
|
+
*/
|
|
514
|
+
function insertHookStateBlock(content, configTomlPath, block) {
|
|
515
|
+
const { start, end } = buildHookStateMarkers(configTomlPath);
|
|
516
|
+
const startIdx = content.indexOf(start);
|
|
517
|
+
if (startIdx !== -1) {
|
|
518
|
+
const endIdx = content.indexOf(end, startIdx + start.length);
|
|
519
|
+
if (endIdx !== -1) {
|
|
520
|
+
const before = content.substring(0, startIdx);
|
|
521
|
+
const after = content.substring(endIdx + end.length);
|
|
522
|
+
return `${before}${block}${after}`;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
|
|
526
|
+
return `${content}${separator}${block}\n`;
|
|
295
527
|
}
|
|
296
528
|
//# sourceMappingURL=codex.js.map
|
|
@@ -8,8 +8,56 @@
|
|
|
8
8
|
* Design principle: no keyword matching, no auto-save — the LLM decides.
|
|
9
9
|
*/
|
|
10
10
|
import path from 'path';
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
11
12
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
12
13
|
import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
|
|
14
|
+
/** Level 2 兼容用的关键词(与 deviation-check 中 USER_CORRECTION_KEYWORDS/REQUIREMENT_CHANGE_KEYWORDS 保持同步) */
|
|
15
|
+
const FALLBACK_CORRECTION_KEYWORDS = ['不对', '错了', '不是这样', '改成', '重做', '错误', '有问题', '有 bug'];
|
|
16
|
+
const FALLBACK_REQUIREMENT_KEYWORDS = ['需求改', '需求变更', '需求调整', '新增需求', '补充需求'];
|
|
17
|
+
async function readCorrectionStats(cwd, changeId) {
|
|
18
|
+
if (!changeId)
|
|
19
|
+
return null;
|
|
20
|
+
const userInputsPath = path.join(cwd, 'zhuanspec', 'changes', changeId, 'metrics', 'user_inputs.json');
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(userInputsPath, 'utf8');
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
const inputs = Array.isArray(parsed.inputs) ? parsed.inputs : [];
|
|
25
|
+
if (inputs.length === 0)
|
|
26
|
+
return { level: 3, hardCorrectionCount: 0, requirementChangeCount: 0 };
|
|
27
|
+
const hasSignal = inputs.some((i) => i && typeof i === 'object' && 'correctionSignal' in i);
|
|
28
|
+
if (hasSignal) {
|
|
29
|
+
let hard = 0;
|
|
30
|
+
let req = 0;
|
|
31
|
+
for (const entry of inputs) {
|
|
32
|
+
const sig = entry['correctionSignal'];
|
|
33
|
+
const postBaseline = entry['postBaseline'] === true;
|
|
34
|
+
if (!sig || sig.kind !== 'hard' || !postBaseline)
|
|
35
|
+
continue;
|
|
36
|
+
if (sig.isRequirementChange)
|
|
37
|
+
req += 1;
|
|
38
|
+
else
|
|
39
|
+
hard += 1;
|
|
40
|
+
}
|
|
41
|
+
return { level: 1, hardCorrectionCount: hard, requirementChangeCount: req };
|
|
42
|
+
}
|
|
43
|
+
// Level 2: 按 summary 关键词推断
|
|
44
|
+
let hard2 = 0;
|
|
45
|
+
let req2 = 0;
|
|
46
|
+
for (const entry of inputs) {
|
|
47
|
+
const summary = String(entry['summary'] || '').toLowerCase();
|
|
48
|
+
if (!summary)
|
|
49
|
+
continue;
|
|
50
|
+
if (FALLBACK_REQUIREMENT_KEYWORDS.some((k) => summary.includes(k.toLowerCase())))
|
|
51
|
+
req2 += 1;
|
|
52
|
+
else if (FALLBACK_CORRECTION_KEYWORDS.some((k) => summary.includes(k.toLowerCase())))
|
|
53
|
+
hard2 += 1;
|
|
54
|
+
}
|
|
55
|
+
return { level: 2, hardCorrectionCount: hard2, requirementChangeCount: req2 };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
13
61
|
export async function collectKnowledgeHook(options) {
|
|
14
62
|
let stdinData = {};
|
|
15
63
|
try {
|
|
@@ -61,6 +109,8 @@ export async function runCollectKnowledge(filePath) {
|
|
|
61
109
|
const cwd = process.cwd();
|
|
62
110
|
const knowledgeDir = path.join(cwd, 'zhuanspec', 'knowledge');
|
|
63
111
|
const hasKnowledge = await FileSystemUtils.directoryExists(knowledgeDir);
|
|
112
|
+
// 读纠偏统计(三级 fallback,读失败不阻断主流程)
|
|
113
|
+
const stats = await readCorrectionStats(cwd, changeId);
|
|
64
114
|
const knowledgeRef = hasKnowledge
|
|
65
115
|
? '参考 `zhuanspec/knowledge/` 下已有文件的格式(统一 Front Matter + 三类分目录)'
|
|
66
116
|
: '将写入 `zhuanspec/knowledge/troubleshooting/`、`best-practices/` 或 `implicit-conventions/` 对应目录(仅此三类)';
|
|
@@ -84,9 +134,25 @@ export async function runCollectKnowledge(filePath) {
|
|
|
84
134
|
'⚠️ **去重要求**:保存前先读取 `zhuanspec/knowledge/` 下已有文件,',
|
|
85
135
|
'若已有同类记录,跳过或追加到已有文件,不要重复新建。',
|
|
86
136
|
];
|
|
137
|
+
// 纠偏累计提示(基于 user_inputs.json)
|
|
138
|
+
//
|
|
139
|
+
// 阈值选择的启发式意图:
|
|
140
|
+
// - hardCorrectionCount >= 2:一次纠偏不提示避免打扰,两次及以上视为"稳定坑",优先沉淀
|
|
141
|
+
// - requirementChangeCount >= 1:需求变更对设计决策影响通常较大,出现一次即提示
|
|
142
|
+
// 本处仅做"是否优先沉淀"的强度提示,具体三类目录分类一律交给 @skill:zhuanspec:knowledge 的分类决策树,不在本 hook 做分类预判。
|
|
143
|
+
const correctionHints = [];
|
|
144
|
+
if (stats && stats.level <= 2) {
|
|
145
|
+
if (stats.hardCorrectionCount >= 2) {
|
|
146
|
+
correctionHints.push(`⚠️ 本次 apply 已累计 ${stats.hardCorrectionCount} 次纠偏(数据源 Level ${stats.level}),若本次改动是对其中某条纠偏的解决,**请优先沉淀**(具体分类交由 @skill:zhuanspec:knowledge 决定)。`);
|
|
147
|
+
}
|
|
148
|
+
if (stats.requirementChangeCount >= 1) {
|
|
149
|
+
correctionHints.push(`⚠️ 本次 apply 涉及 ${stats.requirementChangeCount} 次需求变更,若本次改动属于其中的设计决策负载,**建议优先沉淀为项目知识**(具体分类交由 @skill:zhuanspec:knowledge 决定)。`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
87
152
|
const additionalContext = [
|
|
88
153
|
`💡 **知识沉淀提示**(针对刚才对 \`${filePath || '该文件'}\` 的改动)`,
|
|
89
154
|
'',
|
|
155
|
+
...(correctionHints.length > 0 ? [...correctionHints, ''] : []),
|
|
90
156
|
...phaseGuide,
|
|
91
157
|
'',
|
|
92
158
|
'询问方式:',
|
|
@@ -15,7 +15,7 @@ interface DeviationCheckOptions {
|
|
|
15
15
|
json?: boolean;
|
|
16
16
|
file?: string;
|
|
17
17
|
tool?: string;
|
|
18
|
-
trigger?: 'pre-tool' | '
|
|
18
|
+
trigger?: 'pre-tool' | 'resume';
|
|
19
19
|
prompt?: string;
|
|
20
20
|
}
|
|
21
21
|
declare const PITFALL_KEYWORDS: string[];
|
|
@@ -24,6 +24,14 @@ declare const IMPLICIT_CONVENTION_KEYWORDS: string[];
|
|
|
24
24
|
declare const USER_CORRECTION_KEYWORDS: string[];
|
|
25
25
|
declare const REQUIREMENT_CHANGE_KEYWORDS: string[];
|
|
26
26
|
export declare function deviationCheckHook(options: DeviationCheckOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* 识别 Codex CLI/App 由 Ambient Suggestions 等后台特性发出的系统内部 prompt。
|
|
29
|
+
* 这类 prompt 并非用户真实输入,不应触发纠偏/需求变更/范围检查。
|
|
30
|
+
*
|
|
31
|
+
* 参考:openai/codex#18541 —— Ambient Suggestions 会调用 UserPromptSubmit 钩子
|
|
32
|
+
* 但不调用对应的 Stop 钩子,且目前无法在项目级关闭。
|
|
33
|
+
*/
|
|
34
|
+
export declare function isCodexInternalPrompt(prompt: string): boolean;
|
|
27
35
|
/**
|
|
28
36
|
* 从提案中提取类名 / 标识符等符号,用于 L2 符号级匹配。
|
|
29
37
|
* 识别规则:
|
|
@@ -33,5 +41,22 @@ export declare function deviationCheckHook(options: DeviationCheckOptions): Prom
|
|
|
33
41
|
* - 排除全小写且无下划线的短词(避免与英文动词擞车)
|
|
34
42
|
*/
|
|
35
43
|
declare function parseEstimatedSymbols(proposalPath: string): Promise<string[]>;
|
|
44
|
+
export declare function extractProposalSummary(content: string, maxLength: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* 构造纠偏四选一上下文(注入给主 agent)
|
|
47
|
+
* 要求主 agent:
|
|
48
|
+
* 1. 停止任何 Edit/Write
|
|
49
|
+
* 2. 用 askUserQuestion 让用户四选一
|
|
50
|
+
* 3. 按选择走对应路径
|
|
51
|
+
* 4. 路径完成后运行 resolve-correction 清理标记
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildCorrectionOptionsContext(proposalSummary: string, changeId: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* 构造需求变更(T2)硬拦截的上下文。
|
|
56
|
+
* 与 buildCorrectionOptionsContext 的区别:
|
|
57
|
+
* - 仅提供 A / D 两个选项(需求变更必然要先改 Spec,无 Bug 豁免路径)
|
|
58
|
+
* - 文案强调“新增/变更功能范围”而非“AI 做错”
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildRequirementChangeContext(proposalSummary: string, changeId: string): string;
|
|
36
61
|
export { PITFALL_KEYWORDS, USER_CORRECTION_KEYWORDS, BEST_PRACTICE_KEYWORDS, IMPLICIT_CONVENTION_KEYWORDS, REQUIREMENT_CHANGE_KEYWORDS, parseEstimatedSymbols };
|
|
37
62
|
//# sourceMappingURL=deviation-check.d.ts.map
|