@planu/cli 5.3.4 → 5.3.6
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/CHANGELOG.md +47 -0
- package/dist/engine/execution/validate-job-executor.js +25 -10
- package/dist/engine/hooks/handlers/on-impl-change.d.ts +17 -4
- package/dist/engine/hooks/handlers/on-impl-change.js +43 -44
- package/dist/engine/living-spec-analyzer.d.ts +5 -2
- package/dist/engine/living-spec-analyzer.js +5 -2
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/reconcile/apply-changes.d.ts +0 -12
- package/dist/engine/reconcile/apply-changes.js +11 -33
- package/dist/engine/reconcile/verify-write.js +2 -3
- package/dist/engine/rules-reconciler.js +21 -2
- package/dist/engine/scope-boundaries/contradiction-checker.js +20 -13
- package/dist/engine/spec-format/acceptance-criteria.js +11 -2
- package/dist/engine/spec-migrator/filesystem-import.js +113 -73
- package/dist/engine/spec-migrator/frontmatter-parser.js +15 -13
- package/dist/engine/universal-rules/catalog.js +6 -0
- package/dist/engine/universal-rules/rules/agent-teams.js +0 -2
- package/dist/engine/universal-rules/rules/planu-approval-gates.js +0 -1
- package/dist/engine/universal-rules/rules/planu-bdd-criteria.js +0 -1
- package/dist/engine/universal-rules/rules/planu-clean-code-no-comments.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-clean-code-no-comments.js +30 -0
- package/dist/engine/universal-rules/rules/planu-debate-review.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-debate-review.js +38 -0
- package/dist/engine/universal-rules/rules/planu-dogfood-bugs.js +0 -2
- package/dist/engine/universal-rules/rules/planu-english-specs.js +0 -1
- package/dist/engine/universal-rules/rules/planu-minimal-change.js +0 -2
- package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
- package/dist/engine/universal-rules/rules/planu-release-policy.js +0 -1
- package/dist/engine/universal-rules/rules/planu-revert-proof-tests.d.ts +3 -0
- package/dist/engine/universal-rules/rules/planu-revert-proof-tests.js +34 -0
- package/dist/engine/universal-rules/rules/planu-sdd-model-routing.js +0 -1
- package/dist/engine/universal-rules/rules/planu-workflow.js +0 -2
- package/dist/engine/validator/validation-report-writer.js +1 -1
- package/dist/tools/auto-reconcile.js +1 -1
- package/dist/tools/challenge-spec/challenge-report.js +1 -1
- package/dist/tools/init-project/portable-index-reconciler.js +2 -1
- package/dist/tools/reconcile-spec.d.ts +1 -1
- package/dist/tools/reconcile-spec.js +306 -179
- package/dist/tools/safe-handler.js +0 -2
- package/dist/tools/start-hooks/engine.d.ts +2 -3
- package/dist/tools/start-hooks/engine.js +50 -33
- package/dist/tools/start-hooks.d.ts +1 -1
- package/dist/tools/start-hooks.js +1 -2
- package/dist/tools/tool-registry/group-infra.js +36 -0
- package/dist/tools/update-status/dod-gates.js +3 -3
- package/dist/tools/update-status/transition-guard.js +17 -4
- package/dist/types/file-hooks.d.ts +4 -6
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/mcp.d.ts +0 -19
- package/dist/types/reconcile.d.ts +5 -17
- package/dist/types/reconcile.js +1 -7
- package/dist/types/spec/core.d.ts +5 -0
- package/dist/types/spec/inputs.d.ts +16 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/hooks/agent-hook-migrator.d.ts +0 -9
- package/dist/engine/hooks/agent-hook-migrator.js +0 -140
- package/dist/types/hook-status-update.d.ts +0 -10
- package/dist/types/hook-status-update.js +0 -3
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
// spec-migrator/filesystem-import.ts — Import specs from disk into the store
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { calculateEstimation } from '../estimator.js';
|
|
4
5
|
import { parseFrontmatterLocal, validSpecType, validSpecScope, validSpecStatus, validRiskLevel, validSpecTarget, validDifficulty, } from './frontmatter-parser.js';
|
|
6
|
+
const RECOMMENDED_MODELS = new Set(['opus', 'sonnet', 'mixed']);
|
|
7
|
+
const EXECUTION_MODES = new Set(['local', 'template', 'light-llm', 'full-llm']);
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null;
|
|
10
|
+
}
|
|
11
|
+
function isFiniteNonNegative(value) {
|
|
12
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
13
|
+
}
|
|
14
|
+
function isValidTokenStrategy(value) {
|
|
15
|
+
return (isPlainObject(value) &&
|
|
16
|
+
typeof value.mode === 'string' &&
|
|
17
|
+
EXECUTION_MODES.has(value.mode) &&
|
|
18
|
+
typeof value.reasoning === 'string' &&
|
|
19
|
+
isFiniteNonNegative(value.estimatedTokens) &&
|
|
20
|
+
typeof value.savings === 'string');
|
|
21
|
+
}
|
|
22
|
+
function isValidPortableEstimation(value) {
|
|
23
|
+
return (isPlainObject(value) &&
|
|
24
|
+
isFiniteNonNegative(value.devHours) &&
|
|
25
|
+
value.devHours > 0 &&
|
|
26
|
+
isFiniteNonNegative(value.reviewHours) &&
|
|
27
|
+
typeof value.recommendedModel === 'string' &&
|
|
28
|
+
RECOMMENDED_MODELS.has(value.recommendedModel) &&
|
|
29
|
+
isFiniteNonNegative(value.tokensOpus) &&
|
|
30
|
+
isFiniteNonNegative(value.tokensSonnet) &&
|
|
31
|
+
isFiniteNonNegative(value.apiCostUsd) &&
|
|
32
|
+
isFiniteNonNegative(value.hourlyRate) &&
|
|
33
|
+
isFiniteNonNegative(value.humanCostUsd) &&
|
|
34
|
+
isFiniteNonNegative(value.totalCostUsd) &&
|
|
35
|
+
isValidTokenStrategy(value.tokenOptimization));
|
|
36
|
+
}
|
|
5
37
|
const SPEC_FOLDER_ID_RE = /^(SPEC-\d{3,})/;
|
|
6
38
|
function optionalString(value) {
|
|
7
39
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
@@ -50,14 +82,20 @@ function generationFromMetadata(metadata, fallbackGeneratedAt) {
|
|
|
50
82
|
...(modelId ? { modelId } : {}),
|
|
51
83
|
};
|
|
52
84
|
}
|
|
53
|
-
// eslint-disable-next-line
|
|
54
|
-
function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metadata) {
|
|
85
|
+
// eslint-disable-next-line complexity -- portable metadata is normalized at one boundary
|
|
86
|
+
function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metadata, globalConfig) {
|
|
55
87
|
const now = new Date().toISOString();
|
|
56
88
|
const folder = join(projectPath, 'planu', 'specs', entryName);
|
|
57
89
|
const specPath = join(folder, 'spec.md');
|
|
58
90
|
const slug = entryName.replace(/^SPEC-\d{3,}-/, '');
|
|
59
91
|
const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
60
92
|
const generation = generationFromMetadata(metadata, now);
|
|
93
|
+
const type = validSpecType(metadata.type) ?? 'feature';
|
|
94
|
+
const scope = validSpecScope(metadata.scope) ?? 'feature';
|
|
95
|
+
const difficulty = validDifficulty(metadata.difficulty) ?? 3;
|
|
96
|
+
const estimation = isValidPortableEstimation(metadata.estimation)
|
|
97
|
+
? metadata.estimation
|
|
98
|
+
: calculateEstimation(type, scope, difficulty, globalConfig);
|
|
61
99
|
return {
|
|
62
100
|
id: specId,
|
|
63
101
|
...(optionalString(metadata.uuid) ? { uuid: optionalString(metadata.uuid) } : {}),
|
|
@@ -71,10 +109,10 @@ function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metada
|
|
|
71
109
|
title: typeof metadata.title === 'string' ? metadata.title : specId,
|
|
72
110
|
slug,
|
|
73
111
|
...(optionalString(metadata.feature) ? { feature: optionalString(metadata.feature) } : {}),
|
|
74
|
-
type
|
|
75
|
-
scope
|
|
112
|
+
type,
|
|
113
|
+
scope,
|
|
76
114
|
status: validSpecStatus(metadata.status) ?? 'draft',
|
|
77
|
-
difficulty
|
|
115
|
+
difficulty,
|
|
78
116
|
risk: validRiskLevel(metadata.risk) ?? 'medium',
|
|
79
117
|
target: validSpecTarget(metadata.target) ?? 'backend',
|
|
80
118
|
projectId,
|
|
@@ -83,23 +121,7 @@ function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metada
|
|
|
83
121
|
specPath,
|
|
84
122
|
technicalPath: specPath,
|
|
85
123
|
progressPath: '',
|
|
86
|
-
estimation
|
|
87
|
-
devHours: 0,
|
|
88
|
-
reviewHours: 0,
|
|
89
|
-
recommendedModel: 'sonnet',
|
|
90
|
-
tokensOpus: 0,
|
|
91
|
-
tokensSonnet: 0,
|
|
92
|
-
apiCostUsd: 0,
|
|
93
|
-
hourlyRate: 0,
|
|
94
|
-
humanCostUsd: 0,
|
|
95
|
-
totalCostUsd: 0,
|
|
96
|
-
tokenOptimization: {
|
|
97
|
-
mode: 'local',
|
|
98
|
-
reasoning: 'imported from filesystem',
|
|
99
|
-
estimatedTokens: 0,
|
|
100
|
-
savings: '',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
124
|
+
estimation,
|
|
103
125
|
actuals: null,
|
|
104
126
|
tags: stringList(metadata.tags),
|
|
105
127
|
dependencies: stringList(metadata.dependencies),
|
|
@@ -125,6 +147,60 @@ function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metada
|
|
|
125
147
|
impactAnalysis: null,
|
|
126
148
|
};
|
|
127
149
|
}
|
|
150
|
+
async function resolveGlobalConfig(deps) {
|
|
151
|
+
return (await deps.getGlobalConfig?.()) ?? {};
|
|
152
|
+
}
|
|
153
|
+
async function importSpecEntry(entry, ctx) {
|
|
154
|
+
const idMatch = SPEC_FOLDER_ID_RE.exec(entry.name);
|
|
155
|
+
if (!idMatch?.[1]) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const specId = idMatch[1];
|
|
159
|
+
const specMdPath = join(ctx.specsRoot, entry.name, 'spec.md');
|
|
160
|
+
const relativeSpecPath = `planu/specs/${entry.name}/spec.md`;
|
|
161
|
+
let spec;
|
|
162
|
+
try {
|
|
163
|
+
const content = await readFile(specMdPath, 'utf-8');
|
|
164
|
+
const { metadata } = parseFrontmatterLocal(content);
|
|
165
|
+
if (ctx.options.strictPortableContract === true && !isValidPortableMetadata(specId, metadata)) {
|
|
166
|
+
ctx.failures.push({ specId, path: relativeSpecPath, code: 'INVALID_CONTRACT' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
spec = buildSpecFromMetadata(specId, entry.name, ctx.projectPath, ctx.projectId, metadata, ctx.globalConfig);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
ctx.failures.push({ specId, path: relativeSpecPath, code: 'READ_FAILED' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const existing = ctx.specsById.get(specId);
|
|
176
|
+
if (existing !== undefined) {
|
|
177
|
+
if (!compatibleIdentity(existing, spec)) {
|
|
178
|
+
ctx.failures.push({ specId, path: relativeSpecPath, code: 'INCOMPATIBLE_IDENTITY' });
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (ctx.storeSpecs.some((stored) => hasAnyUuidIdentity(stored, spec))) {
|
|
183
|
+
ctx.failures.push({ specId, path: relativeSpecPath, code: 'INCOMPATIBLE_IDENTITY' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
await ctx.deps.createSpec(ctx.projectId, spec);
|
|
188
|
+
ctx.specsById.set(specId, spec);
|
|
189
|
+
ctx.importedSpecIds.push(specId);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
const winner = await ctx.deps.getSpecFresh?.(ctx.projectId, specId).catch(() => null);
|
|
193
|
+
if (winner !== undefined && winner !== null && compatibleIdentity(winner, spec)) {
|
|
194
|
+
ctx.specsById.set(specId, winner);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
ctx.failures.push({
|
|
198
|
+
specId,
|
|
199
|
+
path: relativeSpecPath,
|
|
200
|
+
code: winner ? 'INCOMPATIBLE_IDENTITY' : 'CREATE_FAILED',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
128
204
|
/**
|
|
129
205
|
* Import specs that exist on disk (planu/specs/) but are missing from the store.
|
|
130
206
|
* Reads frontmatter from each spec.md and creates a minimal Spec record in the store.
|
|
@@ -136,6 +212,7 @@ export async function importFilesystemSpecs(projectPath, deps, projectId, option
|
|
|
136
212
|
const importedSpecIds = [];
|
|
137
213
|
const storeSpecs = await deps.listSpecs(projectId);
|
|
138
214
|
const specsById = new Map(storeSpecs.map((spec) => [spec.id, spec]));
|
|
215
|
+
const globalConfig = await resolveGlobalConfig(deps);
|
|
139
216
|
let entries;
|
|
140
217
|
try {
|
|
141
218
|
entries = await readdir(specsRoot, { withFileTypes: true });
|
|
@@ -143,58 +220,21 @@ export async function importFilesystemSpecs(projectPath, deps, projectId, option
|
|
|
143
220
|
catch {
|
|
144
221
|
return { importedCount: 0, importedSpecIds, failures, errors: [] };
|
|
145
222
|
}
|
|
223
|
+
const ctx = {
|
|
224
|
+
projectPath,
|
|
225
|
+
projectId,
|
|
226
|
+
deps,
|
|
227
|
+
options,
|
|
228
|
+
specsRoot,
|
|
229
|
+
globalConfig,
|
|
230
|
+
storeSpecs,
|
|
231
|
+
specsById,
|
|
232
|
+
importedSpecIds,
|
|
233
|
+
failures,
|
|
234
|
+
};
|
|
146
235
|
for (const entry of entries) {
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
const idMatch = SPEC_FOLDER_ID_RE.exec(entry.name);
|
|
151
|
-
if (!idMatch?.[1]) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const specId = idMatch[1];
|
|
155
|
-
const specMdPath = join(specsRoot, entry.name, 'spec.md');
|
|
156
|
-
const relativeSpecPath = `planu/specs/${entry.name}/spec.md`;
|
|
157
|
-
let spec;
|
|
158
|
-
try {
|
|
159
|
-
const content = await readFile(specMdPath, 'utf-8');
|
|
160
|
-
const { metadata } = parseFrontmatterLocal(content);
|
|
161
|
-
if (options.strictPortableContract === true && !isValidPortableMetadata(specId, metadata)) {
|
|
162
|
-
failures.push({ specId, path: relativeSpecPath, code: 'INVALID_CONTRACT' });
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
spec = buildSpecFromMetadata(specId, entry.name, projectPath, projectId, metadata);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
failures.push({ specId, path: relativeSpecPath, code: 'READ_FAILED' });
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const existing = specsById.get(specId);
|
|
172
|
-
if (existing !== undefined) {
|
|
173
|
-
if (!compatibleIdentity(existing, spec)) {
|
|
174
|
-
failures.push({ specId, path: relativeSpecPath, code: 'INCOMPATIBLE_IDENTITY' });
|
|
175
|
-
}
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (storeSpecs.some((stored) => hasAnyUuidIdentity(stored, spec))) {
|
|
179
|
-
failures.push({ specId, path: relativeSpecPath, code: 'INCOMPATIBLE_IDENTITY' });
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
await deps.createSpec(projectId, spec);
|
|
184
|
-
specsById.set(specId, spec);
|
|
185
|
-
importedSpecIds.push(specId);
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
const winner = await deps.getSpecFresh?.(projectId, specId).catch(() => null);
|
|
189
|
-
if (winner !== undefined && winner !== null && compatibleIdentity(winner, spec)) {
|
|
190
|
-
specsById.set(specId, winner);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
failures.push({
|
|
194
|
-
specId,
|
|
195
|
-
path: relativeSpecPath,
|
|
196
|
-
code: winner ? 'INCOMPATIBLE_IDENTITY' : 'CREATE_FAILED',
|
|
197
|
-
});
|
|
236
|
+
if (entry.isDirectory()) {
|
|
237
|
+
await importSpecEntry(entry, ctx);
|
|
198
238
|
}
|
|
199
239
|
}
|
|
200
240
|
return {
|
|
@@ -14,8 +14,8 @@ function parseScalar(val) {
|
|
|
14
14
|
if (val === 'null') {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
|
-
if (
|
|
18
|
-
return
|
|
17
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
18
|
+
return Number(val);
|
|
19
19
|
}
|
|
20
20
|
return val.replace(/^["']|["']$/g, '');
|
|
21
21
|
}
|
|
@@ -25,29 +25,31 @@ export function parseFrontmatterLocal(content) {
|
|
|
25
25
|
return { metadata: {} };
|
|
26
26
|
}
|
|
27
27
|
const metadata = {};
|
|
28
|
-
|
|
28
|
+
const stack = [];
|
|
29
29
|
for (const line of match[1].split('\n')) {
|
|
30
30
|
const indent = line.length - line.trimStart().length;
|
|
31
31
|
const trimmed = line.trim();
|
|
32
|
+
if (trimmed.length === 0) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let top = stack.at(-1);
|
|
36
|
+
while (top !== undefined && indent <= top.indent) {
|
|
37
|
+
stack.pop();
|
|
38
|
+
top = stack.at(-1);
|
|
39
|
+
}
|
|
40
|
+
const container = top?.value ?? metadata;
|
|
32
41
|
const object = FM_OBJECT_RE.exec(trimmed);
|
|
33
42
|
if (object?.[1]) {
|
|
34
43
|
const value = {};
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
container[object[1]] = value;
|
|
45
|
+
stack.push({ indent, value });
|
|
37
46
|
continue;
|
|
38
47
|
}
|
|
39
48
|
const kv = FM_KV_RE.exec(trimmed);
|
|
40
49
|
if (!kv?.[1] || kv[2] === undefined) {
|
|
41
50
|
continue;
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
const val = kv[2].trim();
|
|
45
|
-
if (activeObject !== null && indent > activeObject.indent) {
|
|
46
|
-
activeObject.value[key] = parseScalar(val);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
activeObject = null;
|
|
50
|
-
metadata[key] = parseScalar(val);
|
|
52
|
+
container[kv[1]] = parseScalar(kv[2].trim());
|
|
51
53
|
}
|
|
52
54
|
return { metadata };
|
|
53
55
|
}
|
|
@@ -10,6 +10,9 @@ import { planuApprovalGatesRule } from './rules/planu-approval-gates.js';
|
|
|
10
10
|
import { planuReleasePolicyRule } from './rules/planu-release-policy.js';
|
|
11
11
|
import { planuSddModelRoutingRule } from './rules/planu-sdd-model-routing.js';
|
|
12
12
|
import { planuMinimalChangeRule } from './rules/planu-minimal-change.js';
|
|
13
|
+
import { planuDebateReviewRule } from './rules/planu-debate-review.js';
|
|
14
|
+
import { planuRevertProofTestsRule } from './rules/planu-revert-proof-tests.js';
|
|
15
|
+
import { planuCleanCodeNoCommentsRule } from './rules/planu-clean-code-no-comments.js';
|
|
13
16
|
/**
|
|
14
17
|
* The full catalog of universal Planu rules.
|
|
15
18
|
* Order matters: rules are installed in catalog order.
|
|
@@ -25,5 +28,8 @@ export const UNIVERSAL_RULES = [
|
|
|
25
28
|
planuModesRule,
|
|
26
29
|
agentTeamsRule,
|
|
27
30
|
planuDogfoodBugsRule,
|
|
31
|
+
planuDebateReviewRule,
|
|
32
|
+
planuRevertProofTestsRule,
|
|
33
|
+
planuCleanCodeNoCommentsRule,
|
|
28
34
|
];
|
|
29
35
|
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function buildBody() {
|
|
2
|
+
return `# Planu Clean Code — No Comments
|
|
3
|
+
|
|
4
|
+
Auto-generated by \`init_project\`. Do not edit manually.
|
|
5
|
+
|
|
6
|
+
Code must be self-documenting. A comment is allowed only when the behavior cannot be expressed through naming or structure — for example a non-obvious language semantic. Everything else is a naming problem.
|
|
7
|
+
|
|
8
|
+
This rule assumes \`planu-dogfood-bugs\` already prohibits deferred-work markers such as TODO/FIXME and does not restate that prohibition.
|
|
9
|
+
|
|
10
|
+
## The Rule
|
|
11
|
+
|
|
12
|
+
Prohibited: narrative comments that restate the code, \`(SPEC-NNNN: ...)\` rationale blocks whose history belongs in the spec and in git blame, step-by-step or section-divider comments inside a function, and JSDoc that only repeats the signature.
|
|
13
|
+
|
|
14
|
+
Replacement action: Extract a helper whose name is the comment, promote a magic value to a named constant, or introduce a named type when a shape needs explaining.
|
|
15
|
+
|
|
16
|
+
## Enforcement
|
|
17
|
+
|
|
18
|
+
Implementation agents add narrative comments by default. Stripping them before accepting subagent output is part of reviewing that output, not an optional cleanup pass.
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
export const planuCleanCodeNoCommentsRule = {
|
|
22
|
+
id: 'planu-clean-code-no-comments',
|
|
23
|
+
name: 'Planu Clean Code — No Comments',
|
|
24
|
+
description: 'Prohibits narrative comments and rationale blocks, requiring named helpers or constants instead.',
|
|
25
|
+
category: 'quality',
|
|
26
|
+
applicableHosts: ['all'],
|
|
27
|
+
defaultEnabled: true,
|
|
28
|
+
buildContent: (_host) => buildBody(),
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=planu-clean-code-no-comments.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function buildBody() {
|
|
2
|
+
return `# Planu Debate Review
|
|
3
|
+
|
|
4
|
+
Auto-generated by \`init_project\`. Do not edit manually.
|
|
5
|
+
|
|
6
|
+
This rule assumes \`planu-approval-gates\` (reviewer independence, self-approval prohibitions) and \`planu-sdd-model-routing\` (arbitration) and restates neither.
|
|
7
|
+
|
|
8
|
+
## Reviewer Plurality And Provider Diversity
|
|
9
|
+
|
|
10
|
+
Every spec and every implementation is reviewed by two independent reviewers from DIFFERENT providers, in ONE parallel round.
|
|
11
|
+
|
|
12
|
+
Prohibited: running a single reviewer, or running two reviewers from the same provider, and calling the result a debate round.
|
|
13
|
+
|
|
14
|
+
Replacement action: launch both reviewers in the same message so they run concurrently, and pick two different providers (for example one Claude-family model and one external model).
|
|
15
|
+
|
|
16
|
+
## One Parallel Round, No Serial Confirmation Loops
|
|
17
|
+
|
|
18
|
+
Prohibited: re-launching a reviewer for pass 2, pass 3, or a "confirm the fix" loop after the first round already produced verdicts.
|
|
19
|
+
|
|
20
|
+
Replacement action: cross the two verdicts once — coincident findings apply directly, divergent findings are arbitrated against the source code — then close the round. Re-open a reviewer only for a specific disagreement the arbiter cannot resolve by reading the code directly.
|
|
21
|
+
|
|
22
|
+
## Degradation Ladder
|
|
23
|
+
|
|
24
|
+
Prohibited: silently reviewing with only one provider when a second is unavailable and presenting it as a debate round.
|
|
25
|
+
|
|
26
|
+
Replacement action: when only one provider is available, run two isolated sessions of the same model instead of one, so two independent verdicts still exist before arbitration.
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
export const planuDebateReviewRule = {
|
|
30
|
+
id: 'planu-debate-review',
|
|
31
|
+
name: 'Planu Debate Review',
|
|
32
|
+
description: 'Requires two-provider parallel review rounds with no serial confirmation loops and a single-provider degradation ladder.',
|
|
33
|
+
category: 'quality',
|
|
34
|
+
applicableHosts: ['all'],
|
|
35
|
+
defaultEnabled: true,
|
|
36
|
+
buildContent: (_host) => buildBody(),
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=planu-debate-review.js.map
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// engine/universal-rules/rules/planu-dogfood-bugs.ts — SPEC-779
|
|
2
|
-
// Universal rule: Planu Dogfood Bugs (mandatory for all Planu-using projects)
|
|
3
1
|
/** Shared markdown body (same for all hosts). */
|
|
4
2
|
function buildBody() {
|
|
5
3
|
return `# Planu Dogfood Bugs (MANDATORY — STRICT)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function buildBody() {
|
|
2
|
+
return `# Planu Revert-Proof Tests
|
|
3
|
+
|
|
4
|
+
Auto-generated by \`init_project\`. Do not edit manually.
|
|
5
|
+
|
|
6
|
+
A new test that never fails against the unfixed source proves nothing.
|
|
7
|
+
|
|
8
|
+
## The Rule
|
|
9
|
+
|
|
10
|
+
Prohibited: reporting a new test as passing evidence without ever having watched it fail. This includes an assertion that compares a value to itself, a mock that echoes back what it was given, and a negative assertion that holds vacuously regardless of the source under test.
|
|
11
|
+
|
|
12
|
+
Replacement action: before reporting the test as evidence, revert only the source change it covers — invert the specific guard line in place, or run the test against the pre-change body read with \`git show <ref>:<path>\` — run the test, and observe it fail. Then restore the source and confirm the test passes again. Report the observed failure output, not just the final green run.
|
|
13
|
+
|
|
14
|
+
Prohibited: using \`git stash\`, \`git checkout\` or \`git restore\` on a shared checkout to stage the revert. Stashes are a process-wide LIFO stack, so a concurrent agent can pop work that is not its own.
|
|
15
|
+
|
|
16
|
+
A guard's regression test must fail against the unguarded variant specifically, not merely against an unrelated point in history such as HEAD.
|
|
17
|
+
|
|
18
|
+
## Hard Blocks
|
|
19
|
+
|
|
20
|
+
- Do not accept a test whose only run is against already-fixed source.
|
|
21
|
+
- Do not accept an assertion that compares a value to itself.
|
|
22
|
+
- Do not accept an assertion that cannot fail for any input.
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
export const planuRevertProofTestsRule = {
|
|
26
|
+
id: 'planu-revert-proof-tests',
|
|
27
|
+
name: 'Planu Revert-Proof Tests',
|
|
28
|
+
description: 'Requires new tests to be proven to fail against the unfixed source before being reported as evidence.',
|
|
29
|
+
category: 'quality',
|
|
30
|
+
applicableHosts: ['all'],
|
|
31
|
+
defaultEnabled: true,
|
|
32
|
+
buildContent: (_host) => buildBody(),
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=planu-revert-proof-tests.js.map
|
|
@@ -49,7 +49,7 @@ async function reconcileSingleSpec(projectId, specId) {
|
|
|
49
49
|
// Run living-spec analysis for completion update
|
|
50
50
|
const knowledge = await knowledgeStore.getKnowledge(projectId);
|
|
51
51
|
if (knowledge?.projectPath) {
|
|
52
|
-
const report = await analyzeLivingSpec(spec, knowledge.projectPath);
|
|
52
|
+
const report = await analyzeLivingSpec(spec, knowledge.projectPath, { writeProgress: true });
|
|
53
53
|
// Completion percentage update is a safe change
|
|
54
54
|
changes.push(createLogEntry(specId, 'completion-update', `Completion updated to ${String(report.completionPercent)}%`, 'unknown', `${String(report.completionPercent)}%`));
|
|
55
55
|
// Timestamp update is safe
|
|
@@ -102,7 +102,7 @@ export function buildChallengeReport(input) {
|
|
|
102
102
|
], scenarioNames);
|
|
103
103
|
const resolvedScenarios = new Set(resolutionEvidence.map((item) => item.scenario));
|
|
104
104
|
const addressedCount = resolvedScenarios.size;
|
|
105
|
-
const requiredCount = requiredResolvedChallenges(
|
|
105
|
+
const requiredCount = requiredResolvedChallenges(scenarioNames.size);
|
|
106
106
|
return {
|
|
107
107
|
runAt: input.runAt ?? new Date().toISOString(),
|
|
108
108
|
totalScenarios: input.scenarios.length,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { specStore } from '../../storage/index.js';
|
|
1
|
+
import { globalStore, specStore } from '../../storage/index.js';
|
|
2
2
|
import { importFilesystemSpecs } from '../../engine/spec-migrator/filesystem-import.js';
|
|
3
3
|
/** Rebuild the mutable external index from the repository-owned portable contracts. */
|
|
4
4
|
export async function reconcilePortableSpecIndex(projectPath, projectId, deps = {
|
|
5
5
|
listSpecs: specStore.listSpecs,
|
|
6
6
|
createSpec: specStore.createSpec,
|
|
7
7
|
getSpecFresh: specStore.getSpecFresh,
|
|
8
|
+
getGlobalConfig: globalStore.getGlobalConfig,
|
|
8
9
|
}) {
|
|
9
10
|
const result = await importFilesystemSpecs(projectPath, deps, projectId, {
|
|
10
11
|
strictPortableContract: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type ReconcileSpecInput, type ToolResult } from '../types/index.js';
|
|
3
3
|
export declare function handleReconcileSpec(params: ReconcileSpecInput, server?: McpServer): Promise<ToolResult>;
|
|
4
4
|
//# sourceMappingURL=reconcile-spec.d.ts.map
|