peaks-cli 1.4.1 → 2.0.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/.claude-plugin/marketplace.json +51 -0
- package/CHANGELOG.md +238 -0
- package/README-en.md +226 -0
- package/README.md +142 -165
- package/dist/src/cli/commands/agent-commands.d.ts +20 -0
- package/dist/src/cli/commands/agent-commands.js +48 -0
- package/dist/src/cli/commands/audit-commands.d.ts +18 -0
- package/dist/src/cli/commands/audit-commands.js +138 -0
- package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
- package/dist/src/cli/commands/classify-classify-commands.js +151 -0
- package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
- package/dist/src/cli/commands/code-review-commands.js +83 -0
- package/dist/src/cli/commands/config-commands.js +90 -0
- package/dist/src/cli/commands/context-commands.d.ts +21 -0
- package/dist/src/cli/commands/context-commands.js +167 -0
- package/dist/src/cli/commands/core-artifact-commands.js +81 -2
- package/dist/src/cli/commands/hook-handle.js +50 -0
- package/dist/src/cli/commands/loop-commands.d.ts +21 -0
- package/dist/src/cli/commands/loop-commands.js +128 -0
- package/dist/src/cli/commands/memory-commands.d.ts +13 -0
- package/dist/src/cli/commands/memory-commands.js +60 -0
- package/dist/src/cli/commands/openspec-commands.js +37 -0
- package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
- package/dist/src/cli/commands/preferences-commands.js +147 -0
- package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
- package/dist/src/cli/commands/retrospective-commands.js +58 -0
- package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
- package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
- package/dist/src/cli/commands/understand-commands.js +34 -0
- package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
- package/dist/src/cli/commands/upgrade-commands.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +70 -0
- package/dist/src/cli/commands/workspace-commands.js +86 -0
- package/dist/src/cli/program.js +46 -22
- package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
- package/dist/src/services/agent/ecc-agent-service.js +143 -0
- package/dist/src/services/artifacts/request-artifact-service.js +14 -0
- package/dist/src/services/audit/backing-detector.d.ts +24 -0
- package/dist/src/services/audit/backing-detector.js +59 -0
- package/dist/src/services/audit/classifier.d.ts +38 -0
- package/dist/src/services/audit/classifier.js +127 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
- package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
- package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
- package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
- package/dist/src/services/audit/enforcers/lint-style.js +173 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
- package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
- package/dist/src/services/audit/enforcers/login-gate.js +40 -0
- package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
- package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
- package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
- package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
- package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
- package/dist/src/services/audit/red-line-catalog.js +210 -0
- package/dist/src/services/audit/red-lines-service.d.ts +23 -0
- package/dist/src/services/audit/red-lines-service.js +486 -0
- package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
- package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
- package/dist/src/services/audit/static-service.d.ts +57 -0
- package/dist/src/services/audit/static-service.js +125 -0
- package/dist/src/services/audit/types.d.ts +69 -0
- package/dist/src/services/audit/types.js +13 -0
- package/dist/src/services/classify/classify-service.d.ts +42 -0
- package/dist/src/services/classify/classify-service.js +122 -0
- package/dist/src/services/classify/classify-types.d.ts +79 -0
- package/dist/src/services/classify/classify-types.js +90 -0
- package/dist/src/services/code-review/ocr-service.d.ts +129 -0
- package/dist/src/services/code-review/ocr-service.js +362 -0
- package/dist/src/services/config/config-migration.d.ts +32 -0
- package/dist/src/services/config/config-migration.js +92 -0
- package/dist/src/services/config/config-restore.d.ts +10 -0
- package/dist/src/services/config/config-restore.js +47 -0
- package/dist/src/services/config/config-rollback.d.ts +13 -0
- package/dist/src/services/config/config-rollback.js +26 -0
- package/dist/src/services/config/config-service.d.ts +35 -2
- package/dist/src/services/config/config-service.js +81 -0
- package/dist/src/services/config/config-types.d.ts +58 -0
- package/dist/src/services/config/config-types.js +6 -0
- package/dist/src/services/doctor/doctor-service.js +96 -0
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
- package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
- package/dist/src/services/fuzzy-matching/types.js +1 -0
- package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
- package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
- package/dist/src/services/ide/ide-registry.js +7 -0
- package/dist/src/services/ide/ide-types.d.ts +1 -1
- package/dist/src/services/memory/memory-search-service.d.ts +61 -0
- package/dist/src/services/memory/memory-search-service.js +80 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
- package/dist/src/services/preferences/preferences-service.d.ts +6 -0
- package/dist/src/services/preferences/preferences-service.js +43 -0
- package/dist/src/services/preferences/preferences-types.d.ts +90 -0
- package/dist/src/services/preferences/preferences-types.js +38 -0
- package/dist/src/services/recommendations/capability-seed-items.js +0 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
- package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
- package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
- package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
- package/dist/src/services/skills/skill-conformance-service.js +136 -0
- package/dist/src/services/skills/skill-runbook-service.js +44 -10
- package/dist/src/services/skills/sync-service.d.ts +43 -0
- package/dist/src/services/skills/sync-service.js +99 -0
- package/dist/src/services/slice/slice-check-service.js +166 -13
- package/dist/src/services/slice/slice-check-types.d.ts +1 -1
- package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
- package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
- package/dist/src/services/standards/project-context.d.ts +1 -1
- package/dist/src/services/standards/project-context.js +0 -4
- package/dist/src/services/standards/project-standards-service.js +1 -3
- package/dist/src/services/understand/understand-scan-service.js +15 -2
- package/dist/src/services/understand/understand-types.d.ts +26 -0
- package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
- package/dist/src/services/upgrade/1x-detector-service.js +94 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
- package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
- package/dist/src/services/upgrade/upgrade-service.js +381 -0
- package/dist/src/services/workspace/migrate-1-4-1-service.js +1 -1
- package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
- package/dist/src/services/workspace/sid-naming-guard.js +31 -0
- package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
- package/dist/src/services/workspace/workspace-archive-service.js +32 -0
- package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
- package/dist/src/services/workspace/workspace-clean-service.js +86 -0
- package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
- package/dist/src/services/workspace/workspace-state-service.js +43 -0
- package/dist/src/shared/change-id.js +4 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +10 -8
- package/schemas/doctor-report.schema.json +1 -1
- package/scripts/install-skills.mjs +296 -12
- package/skills/peaks-doctor/SKILL.md +59 -0
- package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
- package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
- package/skills/peaks-doctor/test_prompts.json +17 -0
- package/skills/peaks-ide/SKILL.md +2 -0
- package/skills/peaks-qa/SKILL.md +9 -7
- package/skills/peaks-qa/references/artifact-per-request.md +19 -5
- package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
- package/skills/peaks-qa/references/qa-runbook.md +1 -1
- package/skills/peaks-rd/SKILL.md +25 -10
- package/skills/peaks-rd/references/ocr-integration.md +214 -0
- package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
- package/skills/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/peaks-solo/SKILL.md +11 -5
- package/skills/peaks-solo/references/completion-handoff.md +3 -1
- package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
- package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
- package/dist/src/cli/commands/shadcn-commands.js +0 -35
- package/dist/src/cli/commands/skill-context-stats-command.d.ts +0 -40
- package/dist/src/cli/commands/skill-context-stats-command.js +0 -96
- package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -51
- package/dist/src/cli/commands/skill-scope-commands.js +0 -310
- package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
- package/dist/src/services/shadcn/shadcn-service.js +0 -128
- package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
- package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
- package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
- package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
- package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/codex.js +0 -12
- package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
- package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
- package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
- package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/trae.js +0 -12
- package/dist/src/services/skill-scope/detect.d.ts +0 -81
- package/dist/src/services/skill-scope/detect.js +0 -513
- package/dist/src/services/skill-scope/registry.d.ts +0 -41
- package/dist/src/services/skill-scope/registry.js +0 -83
- package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
- package/dist/src/services/skill-scope/source-of-truth.js +0 -118
- package/dist/src/services/skill-scope/types.d.ts +0 -195
- package/dist/src/services/skill-scope/types.js +0 -97
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECC 64 agents soft-optional integration — `peaks agent run`.
|
|
3
|
+
*
|
|
4
|
+
* Per spec §7.2 line 818: "64 agents — Soft Optional — 装了 L3
|
|
5
|
+
* 直接调; 不装 L3 退化到 peaks-cli 自有少数核心诊断". The
|
|
6
|
+
* canonical ECC subprocess contract is:
|
|
7
|
+
*
|
|
8
|
+
* npx ecc consult "<topic>" --target claude
|
|
9
|
+
* npx ecc agent run <agent-name> --target <path> --json
|
|
10
|
+
*
|
|
11
|
+
* The peaks-cli `peaks agent run <name> --target <path> --json`
|
|
12
|
+
* CLI shells out to the second form. When ECC is missing OR
|
|
13
|
+
* `agentShieldEnabled: false`, the audit completes with a
|
|
14
|
+
* peaks-cli-only envelope.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors `static-service.ts` (the ECC AgentShield wrapper from
|
|
17
|
+
* L2.3 P2-a): same `subprocessRunner` injection point, same
|
|
18
|
+
* 5-state reason enum, same soft-fail policy.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
/**
|
|
22
|
+
* The 12 most-used ECC agents per the upstream
|
|
23
|
+
* everything-claude-code catalog. The full 64-agent list is
|
|
24
|
+
* available at runtime via `npx ecc agent list`; this static
|
|
25
|
+
* subset covers the common L3-doctor dispatch paths.
|
|
26
|
+
*/
|
|
27
|
+
export const CANONICAL_ECC_AGENTS = [
|
|
28
|
+
{ name: 'security-reviewer', description: 'Audit trust boundary + OWASP top-10' },
|
|
29
|
+
{ name: 'code-reviewer', description: 'General-purpose code review' },
|
|
30
|
+
{ name: 'typescript-reviewer', description: 'TypeScript-specific review' },
|
|
31
|
+
{ name: 'python-reviewer', description: 'Python-specific review' },
|
|
32
|
+
{ name: 'golang-reviewer', description: 'Go-specific review' },
|
|
33
|
+
{ name: 'rust-reviewer', description: 'Rust-specific review' },
|
|
34
|
+
{ name: 'java-reviewer', description: 'Java-specific review' },
|
|
35
|
+
{ name: 'cpp-reviewer', description: 'C/C++-specific review' },
|
|
36
|
+
{ name: 'backend-patterns', description: 'Backend service patterns' },
|
|
37
|
+
{ name: 'frontend-patterns', description: 'Frontend patterns' },
|
|
38
|
+
{ name: 'database-migrations', description: 'DB migration safety' },
|
|
39
|
+
{ name: 'deployment-patterns', description: 'Deployment / CI-CD patterns' },
|
|
40
|
+
];
|
|
41
|
+
const ECC_DETECT_TIMEOUT_MS = 5000;
|
|
42
|
+
const ECC_RUN_TIMEOUT_MS = 60000;
|
|
43
|
+
const defaultSubprocessRunner = {
|
|
44
|
+
run(command, args, timeoutMs) {
|
|
45
|
+
try {
|
|
46
|
+
const r = spawnSync(command, args, {
|
|
47
|
+
timeout: timeoutMs,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
status: r.status,
|
|
54
|
+
stdout: r.stdout ?? '',
|
|
55
|
+
stderr: r.stderr ?? '',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
status: null,
|
|
61
|
+
stdout: '',
|
|
62
|
+
stderr: '',
|
|
63
|
+
error: err,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function isEccInstalled(runner) {
|
|
69
|
+
const result = runner.run('npx', ['ecc', '--version'], ECC_DETECT_TIMEOUT_MS);
|
|
70
|
+
if (result.error)
|
|
71
|
+
return false;
|
|
72
|
+
return result.status === 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validate the agent name against the canonical ECC catalog.
|
|
76
|
+
* Returns `null` if the agent is known; an error message otherwise.
|
|
77
|
+
*/
|
|
78
|
+
export function validateEccAgent(agent) {
|
|
79
|
+
if (typeof agent !== 'string' || agent.length === 0) {
|
|
80
|
+
return 'peaks agent run: agent name must be a non-empty string';
|
|
81
|
+
}
|
|
82
|
+
if (!/^[a-z][a-z0-9-]*$/.test(agent)) {
|
|
83
|
+
return `peaks agent run: agent name "${agent}" is invalid (must match [a-z][a-z0-9-]*)`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
export function runEccAgent(input, runner = defaultSubprocessRunner) {
|
|
88
|
+
const enableAgent = input.enableAgent === true;
|
|
89
|
+
const warnings = [];
|
|
90
|
+
let result = null;
|
|
91
|
+
// Resolve the effective "should spawn" decision.
|
|
92
|
+
// flagEnabled (CLI override) > preference (always-on for now; L3+ future) > false
|
|
93
|
+
const shouldSpawn = enableAgent;
|
|
94
|
+
const installed = isEccInstalled(runner);
|
|
95
|
+
let reason;
|
|
96
|
+
let spawned;
|
|
97
|
+
if (!shouldSpawn) {
|
|
98
|
+
reason = 'flag-disabled';
|
|
99
|
+
spawned = false;
|
|
100
|
+
}
|
|
101
|
+
else if (!installed) {
|
|
102
|
+
reason = 'flag-enabled-but-ecc-missing';
|
|
103
|
+
spawned = false;
|
|
104
|
+
warnings.push('`npx ecc --version` failed. Run `npx ecc --help` to install ECC, ' +
|
|
105
|
+
'then re-run `peaks agent run`. The peaks-cli native diagnostic ' +
|
|
106
|
+
'still runs via `peaks doctor scan`.');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
reason = 'enabled-and-installed';
|
|
110
|
+
spawned = true;
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
const subResult = runner.run('npx', ['ecc', 'agent', 'run', input.agent, '--target', input.projectRoot, '--json'], ECC_RUN_TIMEOUT_MS);
|
|
113
|
+
let parsed;
|
|
114
|
+
let error;
|
|
115
|
+
if (subResult.error) {
|
|
116
|
+
error = subResult.error.message;
|
|
117
|
+
}
|
|
118
|
+
else if (subResult.status !== 0) {
|
|
119
|
+
error = `ecc agent run exited with status ${subResult.status}: ${subResult.stderr}`;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(subResult.stdout);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Non-JSON output is allowed; peaks-cli surfaces the raw
|
|
127
|
+
// stdout for the human reader and treats the run as
|
|
128
|
+
// ok=true (the subprocess exited 0).
|
|
129
|
+
parsed = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
result = {
|
|
133
|
+
agent: input.agent,
|
|
134
|
+
ok: error === undefined,
|
|
135
|
+
stdout: subResult.stdout,
|
|
136
|
+
stderr: subResult.stderr,
|
|
137
|
+
durationMs: Date.now() - start,
|
|
138
|
+
...(parsed !== undefined ? { parsed } : {}),
|
|
139
|
+
...(error !== undefined ? { error } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { agent: input.agent, spawned, reason, result, warnings };
|
|
143
|
+
}
|
|
@@ -783,6 +783,20 @@ export async function transitionRequestArtifact(options) {
|
|
|
783
783
|
if (!prerequisiteResult.ok && options.allowIncomplete !== true) {
|
|
784
784
|
throw new PrerequisitesNotSatisfiedError(options.role, options.newState, existing.sessionId, prerequisiteResult.missing);
|
|
785
785
|
}
|
|
786
|
+
// L2.1 P0 red line #4: tech-doc-presence. The rd → spec-locked transition
|
|
787
|
+
// is refused if `rd/tech-doc.md` is missing or empty. This is a machine-
|
|
788
|
+
// enforced gate that backs the "MANDATORY tech-doc before spec-locked"
|
|
789
|
+
// prose in the redesign spec §5.4.
|
|
790
|
+
if (options.role === 'rd' && options.newState === 'spec-locked' && options.allowIncomplete !== true) {
|
|
791
|
+
const { checkTechDocPresence, TECH_DOC_MISSING_CODE, TECH_DOC_MISSING_MESSAGE } = await import('../audit/enforcers/tech-doc-presence.js');
|
|
792
|
+
const techDoc = checkTechDocPresence({
|
|
793
|
+
projectRoot: options.projectRoot,
|
|
794
|
+
sessionId: existing.sessionId,
|
|
795
|
+
});
|
|
796
|
+
if (!techDoc.exists || techDoc.isEmpty) {
|
|
797
|
+
throw new PrerequisitesNotSatisfiedError(options.role, options.newState, existing.sessionId, [{ path: techDoc.path, description: `${TECH_DOC_MISSING_CODE}: ${TECH_DOC_MISSING_MESSAGE}` }]);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
786
800
|
// Type sanity check for PRD handoff
|
|
787
801
|
if (options.typeSanityCheck !== undefined && options.role === 'prd' && options.newState === 'handed-off') {
|
|
788
802
|
const sanityReport = checkTypeSanity({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backing detector — classifies each red line as `cli-backed`, `partial`,
|
|
3
|
+
* or `prose-only`. The classifier already sets the backing for catalog hits
|
|
4
|
+
* (cli-backed when an enforcer file path is present). This module exists to
|
|
5
|
+
* handle the post-classification nuance: heuristics for the "partial" tier
|
|
6
|
+
* (a gate exists but the LLM can bypass it) and verification that the
|
|
7
|
+
* enforcer file actually exists on disk.
|
|
8
|
+
*/
|
|
9
|
+
import type { RedLineEntry } from './types.js';
|
|
10
|
+
export interface BackingResult {
|
|
11
|
+
readonly entry: RedLineEntry;
|
|
12
|
+
readonly enforcerExists: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
16
|
+
* `backing` field updated and `enforcerRef` possibly nulled if the
|
|
17
|
+
* referenced file does not exist on disk.
|
|
18
|
+
*/
|
|
19
|
+
export declare function classifyBacking(entry: RedLineEntry, projectRoot: string): BackingResult;
|
|
20
|
+
export interface BackingBatchResult {
|
|
21
|
+
readonly entries: readonly RedLineEntry[];
|
|
22
|
+
readonly warnings: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string): BackingBatchResult;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backing detector — classifies each red line as `cli-backed`, `partial`,
|
|
3
|
+
* or `prose-only`. The classifier already sets the backing for catalog hits
|
|
4
|
+
* (cli-backed when an enforcer file path is present). This module exists to
|
|
5
|
+
* handle the post-classification nuance: heuristics for the "partial" tier
|
|
6
|
+
* (a gate exists but the LLM can bypass it) and verification that the
|
|
7
|
+
* enforcer file actually exists on disk.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
const PARTIAL_PHRASES = [
|
|
12
|
+
'if llm cooperates',
|
|
13
|
+
'llm-cooperation',
|
|
14
|
+
'partial cli backing',
|
|
15
|
+
'best effort',
|
|
16
|
+
'advisory only',
|
|
17
|
+
'soft enforcement',
|
|
18
|
+
'when remembered',
|
|
19
|
+
];
|
|
20
|
+
function detectPartial(context) {
|
|
21
|
+
const lower = context.toLowerCase();
|
|
22
|
+
return PARTIAL_PHRASES.some((phrase) => lower.includes(phrase));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
26
|
+
* `backing` field updated and `enforcerRef` possibly nulled if the
|
|
27
|
+
* referenced file does not exist on disk.
|
|
28
|
+
*/
|
|
29
|
+
export function classifyBacking(entry, projectRoot) {
|
|
30
|
+
if (detectPartial(entry.source.context)) {
|
|
31
|
+
return {
|
|
32
|
+
entry: { ...entry, backing: 'partial' },
|
|
33
|
+
enforcerExists: entry.enforcerRef !== null && existsSync(resolve(projectRoot, entry.enforcerRef)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (entry.enforcerRef === null) {
|
|
37
|
+
return { entry, enforcerExists: false };
|
|
38
|
+
}
|
|
39
|
+
const enforcerPath = resolve(projectRoot, entry.enforcerRef);
|
|
40
|
+
const exists = existsSync(enforcerPath);
|
|
41
|
+
return {
|
|
42
|
+
entry: { ...entry, backing: exists ? 'cli-backed' : 'prose-only' },
|
|
43
|
+
enforcerExists: exists,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function classifyBackingBatch(entries, projectRoot) {
|
|
47
|
+
const updated = [];
|
|
48
|
+
const warnings = [];
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const { entry: reclassified, enforcerExists } = classifyBacking(entry, projectRoot);
|
|
51
|
+
updated.push(reclassified);
|
|
52
|
+
if (reclassified.backing === 'cli-backed' && !enforcerExists) {
|
|
53
|
+
// Defensive: should not happen because classifyBacking downgrades to
|
|
54
|
+
// prose-only, but keep the assertion in case of future drift.
|
|
55
|
+
warnings.push(`enforcer ref "${reclassified.enforcerRef}" missing on disk for ${reclassified.id}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { entries: updated, warnings };
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Red-line classifier — turns raw markdown lines into RedLineEntry.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm: for each MarkdownLine, check whether the line text contains one
|
|
5
|
+
* of the four red-line markers (MANDATORY / BLOCKING / MUST NOT / RED LINE).
|
|
6
|
+
* On a hit, extract the surrounding ±2 lines as context, look up the red
|
|
7
|
+
* line in the catalog, and emit a RedLineEntry. Lines that contain a marker
|
|
8
|
+
* but match no catalog entry still produce a RedLineEntry (with backing =
|
|
9
|
+
* prose-only and enforcerRef = null) — those are the "discovered but not yet
|
|
10
|
+
* enforced" red lines the L2 redesign is working to eliminate.
|
|
11
|
+
*/
|
|
12
|
+
import type { RedLineEntry, RedLineMarker } from './types.js';
|
|
13
|
+
export declare function detectMarker(lineText: string): RedLineMarker | null;
|
|
14
|
+
/**
|
|
15
|
+
* Derive a human-readable rule name from the marker line. Heuristic: take
|
|
16
|
+
* the first 8 words of the line, lowercase, trimmed. Catalog matching is
|
|
17
|
+
* the source of truth for the canonical rule name; this is the fallback
|
|
18
|
+
* when no catalog entry matches.
|
|
19
|
+
*/
|
|
20
|
+
export declare function deriveRuleName(lineText: string): string;
|
|
21
|
+
export interface ClassifyFileInput {
|
|
22
|
+
readonly file: string;
|
|
23
|
+
readonly lines: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
export interface ClassifyResult {
|
|
26
|
+
readonly entries: readonly RedLineEntry[];
|
|
27
|
+
readonly warnings: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Classify a single markdown file. Returns 0+ RedLineEntry; one entry per
|
|
31
|
+
* marker hit. Marker hits that appear multiple times on the same line are
|
|
32
|
+
* counted once.
|
|
33
|
+
*/
|
|
34
|
+
export declare function classifyFile(input: ClassifyFileInput): ClassifyResult;
|
|
35
|
+
/**
|
|
36
|
+
* Batch wrapper — classify each input file and flatten the entries.
|
|
37
|
+
*/
|
|
38
|
+
export declare function classifyFiles(inputs: readonly ClassifyFileInput[]): ClassifyResult;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Red-line classifier — turns raw markdown lines into RedLineEntry.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm: for each MarkdownLine, check whether the line text contains one
|
|
5
|
+
* of the four red-line markers (MANDATORY / BLOCKING / MUST NOT / RED LINE).
|
|
6
|
+
* On a hit, extract the surrounding ±2 lines as context, look up the red
|
|
7
|
+
* line in the catalog, and emit a RedLineEntry. Lines that contain a marker
|
|
8
|
+
* but match no catalog entry still produce a RedLineEntry (with backing =
|
|
9
|
+
* prose-only and enforcerRef = null) — those are the "discovered but not yet
|
|
10
|
+
* enforced" red lines the L2 redesign is working to eliminate.
|
|
11
|
+
*/
|
|
12
|
+
import { findCatalogEntry } from './red-line-catalog.js';
|
|
13
|
+
const MARKER_PATTERN = /\b(MANDATORY|BLOCKING|MUST NOT|RED LINE)\b/;
|
|
14
|
+
const CONTEXT_LINES_BEFORE = 2;
|
|
15
|
+
const CONTEXT_LINES_AFTER = 2;
|
|
16
|
+
function isMarker(s) {
|
|
17
|
+
return s === 'MANDATORY' || s === 'BLOCKING' || s === 'MUST NOT' || s === 'RED LINE';
|
|
18
|
+
}
|
|
19
|
+
function extractContext(allLines, hitLine) {
|
|
20
|
+
const start = Math.max(0, hitLine - CONTEXT_LINES_BEFORE);
|
|
21
|
+
const end = Math.min(allLines.length, hitLine + 1 + CONTEXT_LINES_AFTER);
|
|
22
|
+
return allLines.slice(start, end).join('\n');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Detect markers in a single line of markdown. Returns the marker text found
|
|
26
|
+
* (uppercased), or null when no marker is present.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Case-insensitive marker pattern (used only by detectMarker so callers can
|
|
30
|
+
* write `MANDATORY` / `mandatory` / `Mandatory` interchangeably in prose).
|
|
31
|
+
* deriveRuleName uses the all-caps MARKER_PATTERN so it doesn't strip a
|
|
32
|
+
* mid-sentence "Red Line" reference.
|
|
33
|
+
*/
|
|
34
|
+
const MARKER_PATTERN_CI = /\b(MANDATORY|BLOCKING|MUST NOT|RED LINE)\b/i;
|
|
35
|
+
export function detectMarker(lineText) {
|
|
36
|
+
const match = MARKER_PATTERN_CI.exec(lineText);
|
|
37
|
+
if (!match)
|
|
38
|
+
return null;
|
|
39
|
+
const raw = (match[1] ?? '').toUpperCase();
|
|
40
|
+
if (raw === 'MUST') {
|
|
41
|
+
// "MUST NOT" is two tokens in the regex; the match group only captures
|
|
42
|
+
// "MUST". Re-verify by checking the next character.
|
|
43
|
+
const after = lineText[match.index + match[0].length];
|
|
44
|
+
if (after === undefined || /\s/.test(after)) {
|
|
45
|
+
return 'MUST NOT';
|
|
46
|
+
}
|
|
47
|
+
return isMarker(raw) ? raw : null;
|
|
48
|
+
}
|
|
49
|
+
return isMarker(raw) ? raw : null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Derive a human-readable rule name from the marker line. Heuristic: take
|
|
53
|
+
* the first 8 words of the line, lowercase, trimmed. Catalog matching is
|
|
54
|
+
* the source of truth for the canonical rule name; this is the fallback
|
|
55
|
+
* when no catalog entry matches.
|
|
56
|
+
*/
|
|
57
|
+
export function deriveRuleName(lineText) {
|
|
58
|
+
const cleaned = lineText
|
|
59
|
+
.replace(MARKER_PATTERN, '')
|
|
60
|
+
.replace(/^[*_`#>:]+/, '')
|
|
61
|
+
.replace(/[*_`#>]/g, '')
|
|
62
|
+
.trim();
|
|
63
|
+
const words = cleaned.split(/\s+/).filter(Boolean).slice(0, 8);
|
|
64
|
+
return words.length > 0 ? words.join(' ').toLowerCase() : 'unspecified red line';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Classify a single markdown file. Returns 0+ RedLineEntry; one entry per
|
|
68
|
+
* marker hit. Marker hits that appear multiple times on the same line are
|
|
69
|
+
* counted once.
|
|
70
|
+
*/
|
|
71
|
+
export function classifyFile(input) {
|
|
72
|
+
const entries = [];
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (let idx = 0; idx < input.lines.length; idx++) {
|
|
76
|
+
const lineText = input.lines[idx] ?? '';
|
|
77
|
+
if (seen.has(idx + 1))
|
|
78
|
+
continue;
|
|
79
|
+
const marker = detectMarker(lineText);
|
|
80
|
+
if (marker === null)
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(idx + 1);
|
|
83
|
+
const context = extractContext(input.lines, idx);
|
|
84
|
+
const ruleName = deriveRuleName(lineText);
|
|
85
|
+
const markers = [marker];
|
|
86
|
+
const catalog = findCatalogEntry(ruleName, markers);
|
|
87
|
+
const source = {
|
|
88
|
+
file: input.file,
|
|
89
|
+
line: idx + 1,
|
|
90
|
+
marker,
|
|
91
|
+
context,
|
|
92
|
+
};
|
|
93
|
+
if (catalog) {
|
|
94
|
+
entries.push({
|
|
95
|
+
id: catalog.id,
|
|
96
|
+
rule: catalog.rule,
|
|
97
|
+
source,
|
|
98
|
+
backing: catalog.enforcerRef === null ? 'prose-only' : 'cli-backed',
|
|
99
|
+
enforcerRef: catalog.enforcerRef,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Marker hit but no catalog match: discovered, not yet enforced.
|
|
104
|
+
entries.push({
|
|
105
|
+
id: `rl-discovered-${input.file.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-${idx + 1}`,
|
|
106
|
+
rule: ruleName,
|
|
107
|
+
source,
|
|
108
|
+
backing: 'prose-only',
|
|
109
|
+
enforcerRef: null,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { entries, warnings };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Batch wrapper — classify each input file and flatten the entries.
|
|
117
|
+
*/
|
|
118
|
+
export function classifyFiles(inputs) {
|
|
119
|
+
const allEntries = [];
|
|
120
|
+
const allWarnings = [];
|
|
121
|
+
for (const input of inputs) {
|
|
122
|
+
const result = classifyFile(input);
|
|
123
|
+
allEntries.push(...result.entries);
|
|
124
|
+
allWarnings.push(...result.warnings);
|
|
125
|
+
}
|
|
126
|
+
return { entries: allEntries, warnings: allWarnings };
|
|
127
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* active-skill-resolver — utility for hook enforcers.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the active peak skill name for the current session, so hook
|
|
5
|
+
* enforcers (e.g. solo-code-ban) can decide whether to fire.
|
|
6
|
+
*
|
|
7
|
+
* Per `src/services/session/caller-id-types.ts`: the active-skill file is
|
|
8
|
+
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (graceful degradation — never throws):
|
|
11
|
+
* 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
|
|
12
|
+
* 2. .peaks/_runtime/<sid>/active-skill-<callerId>.json for each caller
|
|
13
|
+
* bound to the current peak session
|
|
14
|
+
* 3. null (caller did not set a skill; enforcers can decide to skip)
|
|
15
|
+
*/
|
|
16
|
+
export interface ActiveSkillResolution {
|
|
17
|
+
readonly skill: string | null;
|
|
18
|
+
readonly callerId: string | null;
|
|
19
|
+
readonly sessionId: string | null;
|
|
20
|
+
readonly source: 'env' | 'file' | 'none';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the active peak skill for the current hook invocation.
|
|
24
|
+
*
|
|
25
|
+
* Reads PEAKS_ACTIVE_SKILL first (test override), then walks
|
|
26
|
+
* `.peaks/_runtime/<sid>/` for any `active-skill-*.json` file. Returns
|
|
27
|
+
* the first match.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveActiveSkillForCaller(projectRoot: string): ActiveSkillResolution;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* active-skill-resolver — utility for hook enforcers.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the active peak skill name for the current session, so hook
|
|
5
|
+
* enforcers (e.g. solo-code-ban) can decide whether to fire.
|
|
6
|
+
*
|
|
7
|
+
* Per `src/services/session/caller-id-types.ts`: the active-skill file is
|
|
8
|
+
* at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (graceful degradation — never throws):
|
|
11
|
+
* 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
|
|
12
|
+
* 2. .peaks/_runtime/<sid>/active-skill-<callerId>.json for each caller
|
|
13
|
+
* bound to the current peak session
|
|
14
|
+
* 3. null (caller did not set a skill; enforcers can decide to skip)
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { getSessionIdCanonical } from '../../session/session-manager.js';
|
|
19
|
+
import { getSessionDir } from '../../session/getSessionDir.js';
|
|
20
|
+
const ACTIVE_SKILL_PREFIX = 'active-skill-';
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the active peak skill for the current hook invocation.
|
|
23
|
+
*
|
|
24
|
+
* Reads PEAKS_ACTIVE_SKILL first (test override), then walks
|
|
25
|
+
* `.peaks/_runtime/<sid>/` for any `active-skill-*.json` file. Returns
|
|
26
|
+
* the first match.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveActiveSkillForCaller(projectRoot) {
|
|
29
|
+
const envOverride = process.env.PEAKS_ACTIVE_SKILL;
|
|
30
|
+
if (typeof envOverride === 'string' && envOverride.length > 0) {
|
|
31
|
+
return { skill: envOverride, callerId: null, sessionId: null, source: 'env' };
|
|
32
|
+
}
|
|
33
|
+
let sessionId = null;
|
|
34
|
+
try {
|
|
35
|
+
sessionId = getSessionIdCanonical(projectRoot);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { skill: null, callerId: null, sessionId: null, source: 'none' };
|
|
39
|
+
}
|
|
40
|
+
if (sessionId === null) {
|
|
41
|
+
return { skill: null, callerId: null, sessionId: null, source: 'none' };
|
|
42
|
+
}
|
|
43
|
+
const sessionDir = getSessionDir(projectRoot, sessionId);
|
|
44
|
+
if (!existsSync(sessionDir)) {
|
|
45
|
+
return { skill: null, callerId: null, sessionId: sessionId, source: 'none' };
|
|
46
|
+
}
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = readdirSync(sessionDir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { skill: null, callerId: null, sessionId: sessionId, source: 'none' };
|
|
53
|
+
}
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry.startsWith(ACTIVE_SKILL_PREFIX) || !entry.endsWith('.json'))
|
|
56
|
+
continue;
|
|
57
|
+
const callerId = entry.slice(ACTIVE_SKILL_PREFIX.length, -'.json'.length);
|
|
58
|
+
const filePath = join(sessionDir, entry);
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (typeof parsed.skill === 'string' && parsed.skill.length > 0) {
|
|
63
|
+
return { skill: parsed.skill, callerId, sessionId, source: 'file' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// skip malformed file
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { skill: null, callerId: null, sessionId, source: 'none' };
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-draft-confirm enforcer (L2.2 P1) — verifies a design draft exists
|
|
3
|
+
* and has been confirmed before the rd implementation phase begins.
|
|
4
|
+
*
|
|
5
|
+
* Two red lines:
|
|
6
|
+
* - rl-design-draft-confirm-001: design-draft.md must exist before spec-locked
|
|
7
|
+
* - rl-design-draft-confirm-002: design-draft must have a 'confirmed' marker
|
|
8
|
+
*
|
|
9
|
+
* The state-machine check is wired into peaks request transition (the
|
|
10
|
+
* spec-locked transition requires both files to exist + design to be
|
|
11
|
+
* confirmed). The catalog flags the entry as cli-backed when the
|
|
12
|
+
* enforcer file exists.
|
|
13
|
+
*/
|
|
14
|
+
export interface DesignDraftConfirmInput {
|
|
15
|
+
readonly projectRoot: string;
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
readonly changeId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DesignDraftConfirmResult {
|
|
20
|
+
readonly draftExists: boolean;
|
|
21
|
+
readonly draftPath: string;
|
|
22
|
+
readonly confirmed: boolean;
|
|
23
|
+
readonly confirmationPath: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function checkDesignDraftConfirmation(input: DesignDraftConfirmInput): DesignDraftConfirmResult;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-draft-confirm enforcer (L2.2 P1) — verifies a design draft exists
|
|
3
|
+
* and has been confirmed before the rd implementation phase begins.
|
|
4
|
+
*
|
|
5
|
+
* Two red lines:
|
|
6
|
+
* - rl-design-draft-confirm-001: design-draft.md must exist before spec-locked
|
|
7
|
+
* - rl-design-draft-confirm-002: design-draft must have a 'confirmed' marker
|
|
8
|
+
*
|
|
9
|
+
* The state-machine check is wired into peaks request transition (the
|
|
10
|
+
* spec-locked transition requires both files to exist + design to be
|
|
11
|
+
* confirmed). The catalog flags the entry as cli-backed when the
|
|
12
|
+
* enforcer file exists.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
const CONFIRMATION_MARKERS = [
|
|
17
|
+
/\bconfirmed\b\s*[:=]\s*true/i,
|
|
18
|
+
/\bstatus:\s*confirmed-by-user/i,
|
|
19
|
+
/^#\s*confirmed\b/im,
|
|
20
|
+
];
|
|
21
|
+
export function checkDesignDraftConfirmation(input) {
|
|
22
|
+
// Design drafts live at .peaks/<changeId>/ui/design-draft.md (UI role) or
|
|
23
|
+
// .peaks/<changeId>/prd/requests/<rid>.md (PRD). For L2.2 the canonical
|
|
24
|
+
// location is the UI design-draft.
|
|
25
|
+
const draftPath = join(input.projectRoot, '.peaks', input.changeId, 'ui/design-draft.md');
|
|
26
|
+
const draftExists = existsSync(draftPath);
|
|
27
|
+
if (!draftExists) {
|
|
28
|
+
return {
|
|
29
|
+
draftExists: false,
|
|
30
|
+
draftPath,
|
|
31
|
+
confirmed: false,
|
|
32
|
+
confirmationPath: '',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let content;
|
|
36
|
+
try {
|
|
37
|
+
content = readFileSync(draftPath, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return {
|
|
41
|
+
draftExists: true,
|
|
42
|
+
draftPath,
|
|
43
|
+
confirmed: false,
|
|
44
|
+
confirmationPath: draftPath,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const confirmed = CONFIRMATION_MARKERS.some((p) => p.test(content));
|
|
48
|
+
return {
|
|
49
|
+
draftExists: true,
|
|
50
|
+
draftPath,
|
|
51
|
+
confirmed,
|
|
52
|
+
confirmationPath: draftPath,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LintHit } from './lint-style.js';
|
|
2
|
+
export declare const CATALOG_STABILITY_GROWTH_CAP = 0.2;
|
|
3
|
+
export declare const CATALOG_STABILITY_WINDOW_DAYS = 90;
|
|
4
|
+
export declare const RUNTIME_BUDGET_MS = 2000;
|
|
5
|
+
export interface CatalogStabilityInput {
|
|
6
|
+
/** Current catalog size (entries.length). */
|
|
7
|
+
readonly currentSize: number;
|
|
8
|
+
/** Catalog size 90 days ago, or null if no history available. */
|
|
9
|
+
readonly sizeNinetyDaysAgo: number | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function lintCatalogStability(input: CatalogStabilityInput): readonly LintHit[];
|
|
12
|
+
export declare function lintNoOrphanEnforcer(projectRoot: string): readonly LintHit[];
|
|
13
|
+
export declare function lintNoOrphanCatalog(): readonly LintHit[];
|
|
14
|
+
export declare function lintRuntimeBudget(projectRoot: string, observedMs: number): readonly LintHit[];
|
|
15
|
+
/**
|
|
16
|
+
* Read the catalog-stability history file (if it exists). The
|
|
17
|
+
* file is a small JSON document maintained by the release
|
|
18
|
+
* pipeline; absent → null. We do not invent the historical
|
|
19
|
+
* data; absent data means "soft pass".
|
|
20
|
+
*/
|
|
21
|
+
export declare function readCatalogHistory(projectRoot: string): number | null;
|