@planu/cli 5.5.3 → 5.7.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/CHANGELOG.md +58 -0
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/telemetry.d.ts +3 -0
- package/dist/cli/commands/telemetry.js +118 -0
- package/dist/cli/router.js +3 -1
- package/dist/config/environment-schema.json +14 -0
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +22 -12
- package/dist/engine/contradiction-detector.d.ts +2 -1
- package/dist/engine/contradiction-detector.js +215 -0
- package/dist/engine/detection-utils.d.ts +1 -0
- package/dist/engine/detection-utils.js +33 -0
- package/dist/engine/doc-generator/portal/index.d.ts +1 -1
- package/dist/engine/doc-generator/portal/index.js +1 -1
- package/dist/engine/doc-generator/portal/portal-regenerator.d.ts +8 -3
- package/dist/engine/doc-generator/portal/portal-regenerator.js +16 -7
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +1 -1
- package/dist/engine/evidence-gates/evidence-autofill.js +1 -1
- package/dist/engine/framework-detector.js +8 -6
- package/dist/engine/handoff-artifacts/schemas.js +4 -0
- package/dist/engine/housekeeping/history-log.d.ts +1 -0
- package/dist/engine/housekeeping/history-log.js +58 -3
- package/dist/engine/housekeeping/index.d.ts +2 -1
- package/dist/engine/housekeeping/index.js +2 -1
- package/dist/engine/housekeeping/legacy-planu-demolisher.d.ts +3 -0
- package/dist/engine/housekeeping/legacy-planu-demolisher.js +164 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.d.ts +9 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.js +57 -0
- package/dist/engine/lifecycle-reconciliation.js +87 -40
- package/dist/engine/next-spec-resolver/orchestration-planner.js +1 -1
- package/dist/engine/next-spec-resolver/session-writer.js +1 -1
- package/dist/engine/project-graph/cache.js +23 -3
- package/dist/engine/readiness-checker.js +14 -2
- package/dist/engine/sdd-flow/checkpoints.js +29 -2
- package/dist/engine/session/checkpoint-writer.d.ts +1 -1
- package/dist/engine/session/checkpoint-writer.js +6 -5
- package/dist/engine/session-state/writer.js +4 -3
- package/dist/engine/spec-format/lean-spec-generator.js +1 -1
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +8 -1
- package/dist/engine/spec-migrator/planu-canonical-policy.js +14 -13
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +28 -3
- package/dist/engine/telemetry/error-reporter.d.ts +9 -9
- package/dist/engine/telemetry/error-reporter.js +15 -34
- package/dist/engine/telemetry/event-envelope.d.ts +11 -0
- package/dist/engine/telemetry/event-envelope.js +124 -0
- package/dist/engine/telemetry/telemetry-client.d.ts +8 -1
- package/dist/engine/telemetry/telemetry-client.js +38 -20
- package/dist/engine/telemetry/telemetry-store.d.ts +15 -2
- package/dist/engine/telemetry/telemetry-store.js +73 -2
- package/dist/engine/universal-rules/rules/planu-release-policy.js +1 -1
- package/dist/engine/validator/spec-compliance-runner.d.ts +2 -1
- package/dist/engine/validator/spec-compliance-runner.js +123 -1
- package/dist/hosts/claude-code/ux/mcp-resources.js +7 -23
- package/dist/index.js +26 -0
- package/dist/resources/specs.js +12 -33
- package/dist/storage/current-project.d.ts +3 -0
- package/dist/storage/current-project.js +21 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/migrations/canonical-storage.js +5 -0
- package/dist/storage/retention.d.ts +14 -0
- package/dist/storage/retention.js +279 -0
- package/dist/storage/spec-index.d.ts +23 -0
- package/dist/storage/spec-index.js +123 -0
- package/dist/storage/spec-store.d.ts +8 -3
- package/dist/storage/spec-store.js +76 -6
- package/dist/storage/storage-catalog.js +3 -3
- package/dist/storage/storage-layout.d.ts +8 -0
- package/dist/storage/storage-layout.js +9 -0
- package/dist/storage/transition-log.js +2 -0
- package/dist/tools/challenge-spec.js +35 -19
- package/dist/tools/create-spec.js +10 -0
- package/dist/tools/execute-sdd-flow.js +11 -2
- package/dist/tools/export-spec.js +2 -1
- package/dist/tools/force-status-analytics.js +2 -1
- package/dist/tools/generate-docs-site.js +2 -1
- package/dist/tools/generate-proposal.js +6 -2
- package/dist/tools/init-project/claude-md-generator.js +19 -2
- package/dist/tools/init-project/conventions-writer.d.ts +5 -2
- package/dist/tools/init-project/conventions-writer.js +18 -13
- package/dist/tools/init-project/git-setup.js +9 -0
- package/dist/tools/init-project/handler.js +9 -1
- package/dist/tools/init-project/legacy-planu.d.ts +2 -0
- package/dist/tools/init-project/legacy-planu.js +18 -0
- package/dist/tools/init-project/legacy-root-migration.d.ts +15 -0
- package/dist/tools/init-project/legacy-root-migration.js +213 -0
- package/dist/tools/init-project/runtime-residue.d.ts +2 -0
- package/dist/tools/init-project/runtime-residue.js +11 -0
- package/dist/tools/init-project/schedule-housekeeping.d.ts +2 -0
- package/dist/tools/init-project/schedule-housekeeping.js +8 -0
- package/dist/tools/reconcile-spec.js +29 -2
- package/dist/tools/register-spec-tools/analysis-tools.d.ts +7 -0
- package/dist/tools/register-spec-tools/analysis-tools.js +13 -1
- package/dist/tools/safe-handler.js +6 -12
- package/dist/tools/session-checkpoint.js +1 -1
- package/dist/tools/update-status/index.js +7 -1
- package/dist/tools/update-status-actions.d.ts +7 -1
- package/dist/tools/update-status-actions.js +10 -2
- package/dist/types/handoff-artifacts.d.ts +1 -0
- package/dist/types/housekeeping.d.ts +38 -0
- package/dist/types/housekeeping.js +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/retention.d.ts +12 -0
- package/dist/types/retention.js +3 -0
- package/dist/types/scope.d.ts +23 -0
- package/dist/types/spec/core.d.ts +7 -0
- package/dist/types/spec/index-cache.d.ts +25 -0
- package/dist/types/spec/index-cache.js +3 -0
- package/dist/types/spec/index.d.ts +1 -0
- package/dist/types/spec/index.js +1 -0
- package/dist/types/spec/inputs.d.ts +9 -0
- package/dist/types/spec-format.d.ts +1 -0
- package/dist/types/telemetry.d.ts +39 -1
- package/dist/types/validation-evidence.d.ts +6 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/pending-release-file.mjs +20 -4
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LegacyMigrationResult {
|
|
2
|
+
readonly migrated: readonly string[];
|
|
3
|
+
readonly warnings: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Fold conventions.json, technology-selection.json, and releases/pending.json
|
|
7
|
+
* into project.json, then move context.md/session-context.md/session.json
|
|
8
|
+
* under planu/.runtime. Returns the migrated relative paths and any
|
|
9
|
+
* malformed-legacy-artifact warnings. Best-effort — a failure leaves the
|
|
10
|
+
* remaining legacy artifacts for a later retry.
|
|
11
|
+
*/
|
|
12
|
+
export declare function migrateLegacyPlanuRoot(projectPath: string): Promise<LegacyMigrationResult>;
|
|
13
|
+
/** Runs `migrateLegacyPlanuRoot` only for an already-initialized project; a no-op otherwise. */
|
|
14
|
+
export declare function resolveLegacyMigration(isUpdate: boolean, projectPath: string): Promise<LegacyMigrationResult>;
|
|
15
|
+
//# sourceMappingURL=legacy-root-migration.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// tools/init-project/legacy-root-migration.ts — SPEC-1699
|
|
2
|
+
// Folds legacy planu/ root artifacts into their canonical SPEC-1699 locations:
|
|
3
|
+
// conventions.json and technology-selection.json fold into project.json,
|
|
4
|
+
// releases/pending.json folds into project.json's releases field, and
|
|
5
|
+
// context.md/session-context.md/session.json move under planu/.runtime.
|
|
6
|
+
// Best-effort and idempotent — a project with none of these legacy artifacts
|
|
7
|
+
// is a no-op. A malformed legacy file is left in place for manual repair
|
|
8
|
+
// instead of being silently treated as absent.
|
|
9
|
+
import { readFile, writeFile, rename, rm, mkdir } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { generateConventionsMdIfMissing } from './conventions-writer.js';
|
|
12
|
+
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
13
|
+
import { pathExists } from '../../core/shared/fs.js';
|
|
14
|
+
async function readJsonFile(filePath, isExpectedShape) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await readFile(filePath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { status: 'missing' };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (!isExpectedShape(parsed)) {
|
|
25
|
+
return { status: 'malformed', error: `${filePath} does not have the expected shape` };
|
|
26
|
+
}
|
|
27
|
+
return { status: 'ok', value: parsed };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return { status: 'malformed', error: error instanceof Error ? error.message : String(error) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isJsonObject(value) {
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function isJsonArray(value) {
|
|
37
|
+
return Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function malformedWarning(filePath, result) {
|
|
40
|
+
return result.status === 'malformed'
|
|
41
|
+
? `Legacy artifact ${filePath} is malformed and was left in place: ${result.error}`
|
|
42
|
+
: null;
|
|
43
|
+
}
|
|
44
|
+
async function planConventionsFold(planuDir) {
|
|
45
|
+
const filePath = join(planuDir, 'conventions.json');
|
|
46
|
+
const result = await readJsonFile(filePath, isJsonObject);
|
|
47
|
+
if (result.status !== 'ok') {
|
|
48
|
+
return { step: null, warning: malformedWarning(filePath, result) };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
step: {
|
|
52
|
+
projectField: 'conventions',
|
|
53
|
+
projectValue: result.value,
|
|
54
|
+
pathsToDelete: [filePath],
|
|
55
|
+
migratedPath: 'planu/conventions.json',
|
|
56
|
+
},
|
|
57
|
+
warning: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function planTechnologySelectionFold(planuDir) {
|
|
61
|
+
const filePath = join(planuDir, 'technology-selection.json');
|
|
62
|
+
const result = await readJsonFile(filePath, isJsonObject);
|
|
63
|
+
if (result.status !== 'ok') {
|
|
64
|
+
return { step: null, warning: malformedWarning(filePath, result) };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
step: {
|
|
68
|
+
projectField: 'technologySelection',
|
|
69
|
+
projectValue: result.value,
|
|
70
|
+
pathsToDelete: [filePath],
|
|
71
|
+
migratedPath: 'planu/technology-selection.json',
|
|
72
|
+
},
|
|
73
|
+
warning: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function dedupeReleaseEntriesById(entries) {
|
|
77
|
+
const deduped = new Map();
|
|
78
|
+
const undeduplicated = [];
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const id = isJsonObject(entry) && typeof entry.specId === 'string' ? entry.specId : undefined;
|
|
81
|
+
if (id === undefined) {
|
|
82
|
+
undeduplicated.push(entry);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
deduped.set(id, entry);
|
|
86
|
+
}
|
|
87
|
+
return [...undeduplicated, ...deduped.values()];
|
|
88
|
+
}
|
|
89
|
+
async function planReleasesFold(planuDir, existingReleases) {
|
|
90
|
+
const releasesDir = join(planuDir, 'releases');
|
|
91
|
+
const pendingPath = join(releasesDir, 'pending.json');
|
|
92
|
+
const result = await readJsonFile(pendingPath, isJsonArray);
|
|
93
|
+
if (result.status !== 'ok') {
|
|
94
|
+
return { step: null, warning: malformedWarning(pendingPath, result) };
|
|
95
|
+
}
|
|
96
|
+
const existing = Array.isArray(existingReleases) ? existingReleases : [];
|
|
97
|
+
return {
|
|
98
|
+
step: {
|
|
99
|
+
projectField: 'releases',
|
|
100
|
+
// Deduped by specId so an interrupted-and-retried migration (project.json
|
|
101
|
+
// already updated, planu/releases/ not yet deleted) never duplicates entries.
|
|
102
|
+
projectValue: dedupeReleaseEntriesById([...existing, ...result.value]),
|
|
103
|
+
pathsToDelete: [releasesDir],
|
|
104
|
+
migratedPath: 'planu/releases/pending.json',
|
|
105
|
+
},
|
|
106
|
+
warning: null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function extractConventionsSource(parsed, projectPath) {
|
|
110
|
+
const stackRaw = parsed.stack;
|
|
111
|
+
const stack = stackRaw !== null && typeof stackRaw === 'object' ? stackRaw : {};
|
|
112
|
+
const stackFields = [
|
|
113
|
+
stack.language,
|
|
114
|
+
stack.framework,
|
|
115
|
+
stack.testRunner,
|
|
116
|
+
stack.linter,
|
|
117
|
+
stack.formatter,
|
|
118
|
+
stack.bundler,
|
|
119
|
+
].filter((value) => typeof value === 'string');
|
|
120
|
+
return {
|
|
121
|
+
language: typeof stack.language === 'string' ? stack.language : 'unknown',
|
|
122
|
+
framework: typeof stack.framework === 'string' ? stack.framework : null,
|
|
123
|
+
stack: stackFields,
|
|
124
|
+
projectPath,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function moveUnderRuntime(planuDir, fileName) {
|
|
128
|
+
const source = join(planuDir, fileName);
|
|
129
|
+
if (!(await pathExists(source))) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const runtimeDir = join(planuDir, '.runtime');
|
|
133
|
+
await mkdir(runtimeDir, { recursive: true });
|
|
134
|
+
await rename(source, join(runtimeDir, fileName));
|
|
135
|
+
return `planu/${fileName}`;
|
|
136
|
+
}
|
|
137
|
+
async function planJsonFolds(planuDir, project) {
|
|
138
|
+
const outcomes = await Promise.all([
|
|
139
|
+
planConventionsFold(planuDir),
|
|
140
|
+
planTechnologySelectionFold(planuDir),
|
|
141
|
+
planReleasesFold(planuDir, project.releases),
|
|
142
|
+
]);
|
|
143
|
+
return {
|
|
144
|
+
steps: outcomes
|
|
145
|
+
.map((outcome) => outcome.step)
|
|
146
|
+
.filter((step) => step !== null),
|
|
147
|
+
warnings: outcomes
|
|
148
|
+
.map((outcome) => outcome.warning)
|
|
149
|
+
.filter((warning) => warning !== null),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function foldJsonArtifactsIntoProject(projectPath, planuDir, projectJsonPath, project) {
|
|
153
|
+
const { steps, warnings } = await planJsonFolds(planuDir, project);
|
|
154
|
+
if (steps.length === 0) {
|
|
155
|
+
return { migrated: [], warnings };
|
|
156
|
+
}
|
|
157
|
+
const nextProject = { ...project };
|
|
158
|
+
for (const step of steps) {
|
|
159
|
+
nextProject[step.projectField] = step.projectValue;
|
|
160
|
+
}
|
|
161
|
+
// Write project.json, then regenerate conventions.md, and only then delete
|
|
162
|
+
// the legacy source files — a crash between steps must never leave the
|
|
163
|
+
// canonical record updated with the legacy source already gone unread.
|
|
164
|
+
await writeFile(projectJsonPath, `${JSON.stringify(nextProject, null, 2)}\n`, 'utf-8');
|
|
165
|
+
const conventionsStep = steps.find((step) => step.projectField === 'conventions');
|
|
166
|
+
if (conventionsStep) {
|
|
167
|
+
await generateConventionsMdIfMissing(projectPath, extractConventionsSource(conventionsStep.projectValue, projectPath), true);
|
|
168
|
+
}
|
|
169
|
+
for (const step of steps) {
|
|
170
|
+
for (const pathToDelete of step.pathsToDelete) {
|
|
171
|
+
await rm(pathToDelete, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { migrated: steps.map((step) => step.migratedPath), warnings };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Fold conventions.json, technology-selection.json, and releases/pending.json
|
|
178
|
+
* into project.json, then move context.md/session-context.md/session.json
|
|
179
|
+
* under planu/.runtime. Returns the migrated relative paths and any
|
|
180
|
+
* malformed-legacy-artifact warnings. Best-effort — a failure leaves the
|
|
181
|
+
* remaining legacy artifacts for a later retry.
|
|
182
|
+
*/
|
|
183
|
+
export async function migrateLegacyPlanuRoot(projectPath) {
|
|
184
|
+
const planuDir = join(projectPath, 'planu');
|
|
185
|
+
const projectJsonPath = join(planuDir, 'project.json');
|
|
186
|
+
const migrated = [];
|
|
187
|
+
const warnings = [];
|
|
188
|
+
try {
|
|
189
|
+
const projectResult = await readJsonFile(projectJsonPath, isJsonObject);
|
|
190
|
+
const project = projectResult.status === 'ok' ? projectResult.value : {};
|
|
191
|
+
const folded = await foldJsonArtifactsIntoProject(projectPath, planuDir, projectJsonPath, project);
|
|
192
|
+
migrated.push(...folded.migrated);
|
|
193
|
+
warnings.push(...folded.warnings);
|
|
194
|
+
for (const fileName of ['context.md', 'session-context.md', 'session.json']) {
|
|
195
|
+
const movedPath = await moveUnderRuntime(planuDir, fileName);
|
|
196
|
+
if (movedPath) {
|
|
197
|
+
migrated.push(movedPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
warnings.push(`Legacy migration stopped early: ${error instanceof Error ? error.message : String(error)}`);
|
|
203
|
+
}
|
|
204
|
+
for (const warning of warnings) {
|
|
205
|
+
reportClassifiedDegradation('LEGACY_MIGRATION_ARTIFACT_WARNING', new Error(warning));
|
|
206
|
+
}
|
|
207
|
+
return { migrated, warnings };
|
|
208
|
+
}
|
|
209
|
+
/** Runs `migrateLegacyPlanuRoot` only for an already-initialized project; a no-op otherwise. */
|
|
210
|
+
export async function resolveLegacyMigration(isUpdate, projectPath) {
|
|
211
|
+
return isUpdate ? migrateLegacyPlanuRoot(projectPath) : { migrated: [], warnings: [] };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=legacy-root-migration.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// tools/init-project/runtime-residue.ts — SPEC-1695
|
|
2
|
+
// Fire-and-forget trigger for the runtime-residue sweep, called from init_project
|
|
3
|
+
// so existing client checkouts self-heal without a manual cleanup step.
|
|
4
|
+
import { withAudit } from '../../engine/autopilot/audit-logger.js';
|
|
5
|
+
import { sweepRuntimeResidue } from '../../engine/housekeeping/runtime-residue-sweep.js';
|
|
6
|
+
export function scheduleRuntimeResidueSweep(projectPath) {
|
|
7
|
+
void withAudit(projectPath, 'init_project', 'sweepRuntimeResidue', () => sweepRuntimeResidue(projectPath), (result) => ({ removed: result.removed.length })).catch(() => {
|
|
8
|
+
/* best-effort */
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=runtime-residue.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// tools/init-project/schedule-housekeeping.ts — SPEC-1709
|
|
2
|
+
import { scheduleRuntimeResidueSweep } from './runtime-residue.js';
|
|
3
|
+
import { scheduleLegacyPlanuDemolition } from './legacy-planu.js';
|
|
4
|
+
export function scheduleHousekeepingSweeps(projectPath) {
|
|
5
|
+
scheduleRuntimeResidueSweep(projectPath);
|
|
6
|
+
scheduleLegacyPlanuDemolition(projectPath);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=schedule-housekeeping.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SECTIONS_WITHOUT_LITERAL_BODY_TEXT, } from '../types/index.js';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
|
|
4
4
|
import { ti, t } from '../i18n/index.js';
|
|
5
5
|
import { formatSuccess, addNextSteps } from './response-helpers.js';
|
|
@@ -14,6 +14,9 @@ import { analyzeLivingSpec } from '../engine/living-spec-analyzer.js';
|
|
|
14
14
|
import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js';
|
|
15
15
|
import { applyChangesToSpec } from '../engine/reconcile/apply-changes.js';
|
|
16
16
|
import { verifyWriteSucceeded } from '../engine/reconcile/verify-write.js';
|
|
17
|
+
import { reconcileImplementingSpec, currentReconciliationInvocationContext, } from '../engine/lifecycle-reconciliation.js';
|
|
18
|
+
import { reconciliationDigest } from '../engine/lifecycle-reconciliation-io.js';
|
|
19
|
+
import { syncSpecFiles } from './update-status/file-sync.js';
|
|
17
20
|
function detectEstimationDrift(spec) {
|
|
18
21
|
if (!spec.actuals) {
|
|
19
22
|
return null;
|
|
@@ -322,8 +325,29 @@ function applyConflictResolution(resolution, allChanges, pendingChanges) {
|
|
|
322
325
|
}
|
|
323
326
|
}
|
|
324
327
|
}
|
|
328
|
+
async function routeDeclaredArchitecturalDrift(spec, projectId, projectPath, declaredDrift) {
|
|
329
|
+
const reason = declaredDrift.reason;
|
|
330
|
+
const input = {
|
|
331
|
+
specId: spec.id,
|
|
332
|
+
projectId,
|
|
333
|
+
projectPath,
|
|
334
|
+
status: 'review',
|
|
335
|
+
reconciliationRequestId: randomUUID(),
|
|
336
|
+
expectedImplementingTransitionId: spec.statusHistory?.at(-1)?.transitionId,
|
|
337
|
+
implementationReviewDigest: reconciliationDigest(reason.trim()),
|
|
338
|
+
reason,
|
|
339
|
+
declaredDriftKind: declaredDrift.kind,
|
|
340
|
+
};
|
|
341
|
+
return reconcileImplementingSpec({
|
|
342
|
+
input,
|
|
343
|
+
projectId,
|
|
344
|
+
projectPath,
|
|
345
|
+
context: currentReconciliationInvocationContext(),
|
|
346
|
+
syncSpecFiles,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
325
349
|
export async function handleReconcileSpec(params, server) {
|
|
326
|
-
const { specId, projectId, autoDetect = true, livingSpec = false, changes: manualChanges, } = params;
|
|
350
|
+
const { specId, projectId, autoDetect = true, livingSpec = false, changes: manualChanges, declaredDrift, } = params;
|
|
327
351
|
try {
|
|
328
352
|
const spec = await specStore.getSpec(projectId, specId);
|
|
329
353
|
if (!spec) {
|
|
@@ -333,6 +357,9 @@ export async function handleReconcileSpec(params, server) {
|
|
|
333
357
|
};
|
|
334
358
|
}
|
|
335
359
|
const knowledge = await knowledgeStore.getKnowledge(projectId);
|
|
360
|
+
if (declaredDrift) {
|
|
361
|
+
return await routeDeclaredArchitecturalDrift(spec, projectId, knowledge?.projectPath, declaredDrift);
|
|
362
|
+
}
|
|
336
363
|
const allChanges = [];
|
|
337
364
|
if (autoDetect) {
|
|
338
365
|
const autoChanges = await autoDetectChanges(spec, knowledge);
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const DeclaredDriftInputSchema: z.ZodObject<{
|
|
4
|
+
kind: z.ZodEnum<{
|
|
5
|
+
"architectural-premise": "architectural-premise";
|
|
6
|
+
}>;
|
|
7
|
+
reason: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
2
9
|
/**
|
|
3
10
|
* Registers drift detection, analysis, and discovery tools (tools 11–19) on the MCP server.
|
|
4
11
|
*/
|
|
@@ -7,6 +7,7 @@ import { handleDetectDrift } from '../detect-drift.js';
|
|
|
7
7
|
import { handleSummarizeSpec } from '../summarize-spec.js';
|
|
8
8
|
import { handleGenerateChecklist } from '../generate-checklist.js';
|
|
9
9
|
import { handleReconcileSpec } from '../reconcile-spec.js';
|
|
10
|
+
import { runWithTrustedLocalMcpContext } from '../../engine/lifecycle-reconciliation.js';
|
|
10
11
|
import { handleLearn } from '../learn.js';
|
|
11
12
|
import { handleAudit } from '../audit.js';
|
|
12
13
|
import { handleConsultDocs } from '../consult-docs.js';
|
|
@@ -17,6 +18,16 @@ import { handleSuggestMcpServer } from '../suggest-mcp-server.js';
|
|
|
17
18
|
import { handleScanOrphanSpecRefs, ScanOrphanSpecRefsInputSchema, } from '../scan-orphan-spec-refs.js';
|
|
18
19
|
import { registerGraphSpecsTool } from '../graph-specs.js';
|
|
19
20
|
import { registerAuditSpecsDriftTool } from '../audit-specs-drift.js';
|
|
21
|
+
export const DeclaredDriftInputSchema = z.object({
|
|
22
|
+
kind: z
|
|
23
|
+
.enum(['architectural-premise'])
|
|
24
|
+
.describe('Drift kind. Only "architectural-premise" is supported: an orchestrator-declared premise contradiction that produces no auto-detectable file/scope drift.'),
|
|
25
|
+
reason: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(100)
|
|
28
|
+
.max(10_000)
|
|
29
|
+
.describe('Why the implementation drifted from the approved architectural premise. Minimum 100 characters — becomes the reconciliation transition reason.'),
|
|
30
|
+
});
|
|
20
31
|
import { handleSsrBackMigration } from '../ssr-back-migration.js';
|
|
21
32
|
import { registerMigrateLegacySpecTool } from '../migrate-legacy-spec.js';
|
|
22
33
|
/**
|
|
@@ -130,13 +141,14 @@ export function registerAnalysisTools(server) {
|
|
|
130
141
|
.max(1000)
|
|
131
142
|
.optional()
|
|
132
143
|
.describe('Manual changes to apply'),
|
|
144
|
+
declaredDrift: DeclaredDriftInputSchema.optional().describe('When the spec is implementing, routes reconcile_spec into the existing implementing→review reconciliation demotion instead of auto-detected drift.'),
|
|
133
145
|
},
|
|
134
146
|
}, safeGoverned('reconcile_spec', async (args) => {
|
|
135
147
|
const pid = resolveProjectId(args);
|
|
136
148
|
if (!pid) {
|
|
137
149
|
return missingProjectIdError;
|
|
138
150
|
}
|
|
139
|
-
return handleReconcileSpec({ ...args, projectId: pid });
|
|
151
|
+
return runWithTrustedLocalMcpContext(() => handleReconcileSpec({ ...args, projectId: pid }));
|
|
140
152
|
}));
|
|
141
153
|
// 15. learn_pattern
|
|
142
154
|
server.registerTool('learn_pattern', {
|
|
@@ -7,8 +7,7 @@ import { ensureWorkersStarted } from '../engine/workers/index.js';
|
|
|
7
7
|
import { recordError } from '../storage/error-telemetry-store.js';
|
|
8
8
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
9
9
|
import { reportToolError, reportToolValidationError } from '../engine/telemetry/error-reporter.js';
|
|
10
|
-
import {
|
|
11
|
-
import { PLANU_VERSION } from '../config/version.js';
|
|
10
|
+
import { sendTelemetryEnvelopeEvent } from '../engine/telemetry/telemetry-client.js';
|
|
12
11
|
import { recordToolTokens, extractOutputText } from './token-recording.js';
|
|
13
12
|
import { DriftCacheStore } from '../storage/drift-cache-store.js';
|
|
14
13
|
import { consumeUpdateBanner } from '../engine/update-notifier.js';
|
|
@@ -509,18 +508,13 @@ function safeWithTelemetry(toolName, handler) {
|
|
|
509
508
|
const outputText = extractOutputText(processedResult);
|
|
510
509
|
recordLlmTokens(toolName, inputText, outputText);
|
|
511
510
|
}
|
|
512
|
-
// Emit
|
|
511
|
+
// Emit mcp_tool_completed envelope for successful calls (fire-and-forget)
|
|
513
512
|
/* v8 ignore start */
|
|
514
513
|
if (toolName !== undefined && processedResult.isError !== true) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
planVersion: PLANU_VERSION,
|
|
520
|
-
nodeVersion: process.version,
|
|
521
|
-
platform: process.platform,
|
|
522
|
-
durationMs: Date.now() - startTime,
|
|
523
|
-
},
|
|
514
|
+
sendTelemetryEnvelopeEvent('mcp_tool_completed', {
|
|
515
|
+
toolName,
|
|
516
|
+
result: 'success',
|
|
517
|
+
durationMs: Date.now() - startTime,
|
|
524
518
|
});
|
|
525
519
|
}
|
|
526
520
|
/* v8 ignore stop */
|
|
@@ -9,7 +9,7 @@ import { compactResult } from './output-formatter.js';
|
|
|
9
9
|
export async function handleSessionCheckpoint(args) {
|
|
10
10
|
const snapshot = sessionTracker.getSnapshot();
|
|
11
11
|
await writeCheckpoint(args.projectPath, snapshot);
|
|
12
|
-
return compactResult(`Session checkpoint written to ${args.projectPath}/planu/session-context.md\n` +
|
|
12
|
+
return compactResult(`Session checkpoint written to ${args.projectPath}/planu/.runtime/session-context.md\n` +
|
|
13
13
|
`Calls recorded: ${String(snapshot.callCount)}, ` +
|
|
14
14
|
`files tracked: ${String(snapshot.modifiedFiles.length)}.`);
|
|
15
15
|
}
|
|
@@ -7,7 +7,7 @@ import { rollbackTransitionSpec, transitionSpec, } from '../../engine/spec-state
|
|
|
7
7
|
import { calculateAccuracy } from '../../engine/estimator.js';
|
|
8
8
|
import { cascadeCheck } from '../../engine/spec-versioner.js';
|
|
9
9
|
import { runComplianceGates } from '../update-status-convention-gate.js';
|
|
10
|
-
import { runDoneActions, runDoneSideEffects, runImplementingActions, runImplementingSideEffects, } from '../update-status-actions.js';
|
|
10
|
+
import { runDoneActions, runDoneSideEffects, runImplementingActions, runImplementingSideEffects, pruneRuntimeArtifactsOnTerminal, } from '../update-status-actions.js';
|
|
11
11
|
import { compactObj } from '../../engine/compact-obj.js';
|
|
12
12
|
import { checkTransition, checkDorGate, checkAmbiguityGate, checkReadinessGate, checkChallengeGate, checkImplementationDependencyGate, checkSpecArtifactCommittedGate, resolveAutoAdvanceSteps, isReverseTransition, validateReverseTransition, } from './transition-guard.js';
|
|
13
13
|
import { checkApprovedDepGate } from '../../engine/dep-guard/index.js';
|
|
@@ -1426,6 +1426,12 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1426
1426
|
/* reliability-optional: TERMINAL_VALIDATION_REPORT — canonical receipt is authoritative */
|
|
1427
1427
|
reportClassifiedDegradation('TERMINAL_VALIDATION_REPORT', error);
|
|
1428
1428
|
});
|
|
1429
|
+
try {
|
|
1430
|
+
await pruneRuntimeArtifactsOnTerminal(projectId, specId);
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
reportClassifiedDegradation('RETENTION_TERMINAL_PRUNE', error);
|
|
1434
|
+
}
|
|
1429
1435
|
}
|
|
1430
1436
|
const allSpecs = await specStore.listSpecs(projectId);
|
|
1431
1437
|
const cascadeResult = cascadeCheck(specId, allSpecs, newStatus);
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type { ConstitutionViolation, DoneSideEffectsReport } from '../types/index.js';
|
|
1
|
+
import type { ConstitutionViolation, DoneSideEffectsReport, RetentionResult } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Remove handoff packages, evidence bundles, and archived context for a spec
|
|
4
|
+
* that just reached a terminal status (done or discarded). Never touches
|
|
5
|
+
* planu/specs/ — the canonical spec.md lives outside runtime storage.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pruneRuntimeArtifactsOnTerminal(projectId: string, specId: string): Promise<RetentionResult>;
|
|
2
8
|
export declare function runImplementingActions(projectId: string, specId: string, options?: {
|
|
3
9
|
deferSideEffects?: boolean;
|
|
4
10
|
projectPath?: string;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { specStore, knowledgeStore } from '../storage/index.js';
|
|
1
|
+
import { specStore, knowledgeStore, projectDataDir, enforceRetention } from '../storage/index.js';
|
|
2
2
|
import { checkConstitutionCompliance } from './create-spec-tech.js';
|
|
3
3
|
import { withAudit } from '../engine/autopilot/audit-logger.js';
|
|
4
4
|
import { hasPending, markPending, clearPending } from '../engine/autopilot/cascade-deduplicator.js';
|
|
5
|
+
/**
|
|
6
|
+
* Remove handoff packages, evidence bundles, and archived context for a spec
|
|
7
|
+
* that just reached a terminal status (done or discarded). Never touches
|
|
8
|
+
* planu/specs/ — the canonical spec.md lives outside runtime storage.
|
|
9
|
+
*/
|
|
10
|
+
export async function pruneRuntimeArtifactsOnTerminal(projectId, specId) {
|
|
11
|
+
return enforceRetention({ kind: 'spec-artifacts', path: projectDataDir(projectId), specId });
|
|
12
|
+
}
|
|
5
13
|
function normalizeRejectedReasons(results) {
|
|
6
14
|
return results.map((result) => {
|
|
7
15
|
const reason = result.reason;
|
|
@@ -210,7 +218,7 @@ export async function runDoneActions(projectId, specId, gitBranch, options = {})
|
|
|
210
218
|
// SPEC-649: slim cascade — 2 actions only
|
|
211
219
|
const autopilotSummary = [
|
|
212
220
|
'session.json: refresh queued',
|
|
213
|
-
'
|
|
221
|
+
'project.json releases: refresh queued',
|
|
214
222
|
];
|
|
215
223
|
return {
|
|
216
224
|
mergeWarning,
|
|
@@ -124,6 +124,7 @@ export interface ReconciliationReceiptV1 {
|
|
|
124
124
|
transitionId?: string;
|
|
125
125
|
phase?: 'prepared' | 'status-committed' | 'spec-synced' | 'audit-appended';
|
|
126
126
|
auditPending?: boolean;
|
|
127
|
+
driftSource?: 'declared-architectural-premise';
|
|
127
128
|
}
|
|
128
129
|
/** Injectable receipt persistence boundary used only for reconciliation crash tests. */
|
|
129
130
|
export interface ReconciliationIo {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GlobalProjectsRegistry, RegisteredProject } from './cross-repo-search.js';
|
|
1
2
|
export interface StaleBranchInfo {
|
|
2
3
|
/** Short ref name, e.g. 'tmp-foo' or 'feat/spec-100-bar'. */
|
|
3
4
|
name: string;
|
|
@@ -146,4 +147,41 @@ export interface HousekeepingHistoryEntry {
|
|
|
146
147
|
prevSha: string;
|
|
147
148
|
sha256: string;
|
|
148
149
|
}
|
|
150
|
+
export interface RuntimeResidueSweepResult {
|
|
151
|
+
/** Absolute paths of every legacy runtime artifact removed from planu/. */
|
|
152
|
+
removed: string[];
|
|
153
|
+
}
|
|
154
|
+
export interface LegacyPlanuDemolitionFailure {
|
|
155
|
+
/** Directory name under <legacyRoot>/data/projects. */
|
|
156
|
+
dir: string;
|
|
157
|
+
/** Human-readable reason the directory could not be migrated. */
|
|
158
|
+
reason: string;
|
|
159
|
+
}
|
|
160
|
+
export type LegacyPlanuDemolitionStatus = 'demolished' | 'retained' | 'absent';
|
|
161
|
+
export interface LegacyPlanuDemolitionReport {
|
|
162
|
+
status: LegacyPlanuDemolitionStatus;
|
|
163
|
+
/** Directory names successfully migrated into their canonical destination. */
|
|
164
|
+
migrated: string[];
|
|
165
|
+
/** Count of unmappable directories deleted outright. */
|
|
166
|
+
demolishedUnmappable: number;
|
|
167
|
+
/** Per-directory migration failures; the root is retained when this is non-empty. */
|
|
168
|
+
failures: LegacyPlanuDemolitionFailure[];
|
|
169
|
+
/** Bytes freed by deleting the whole legacy root (0 unless status is 'demolished'). */
|
|
170
|
+
freedBytes: number;
|
|
171
|
+
}
|
|
172
|
+
export interface LegacyPlanuDemolisherOptions {
|
|
173
|
+
/** Legacy root to sweep. Defaults to join(homedir(), '.planu'). */
|
|
174
|
+
legacyRoot?: string;
|
|
175
|
+
/** Global cross-repo registry. Defaults to a live read via getRegistry(). */
|
|
176
|
+
registry?: GlobalProjectsRegistry;
|
|
177
|
+
/** Resolves a registered project's canonical storage destination. */
|
|
178
|
+
resolveDestination?: (project: RegisteredProject) => Promise<string>;
|
|
179
|
+
}
|
|
180
|
+
/** Internal accumulator returned by the data/projects sweep pass. */
|
|
181
|
+
export interface LegacyPlanuSweepResult {
|
|
182
|
+
migrated: string[];
|
|
183
|
+
demolishedUnmappable: number;
|
|
184
|
+
failures: LegacyPlanuDemolitionFailure[];
|
|
185
|
+
freedBytes: number;
|
|
186
|
+
}
|
|
149
187
|
//# sourceMappingURL=housekeeping.d.ts.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -293,5 +293,6 @@ export * from './host-tool-filter.js';
|
|
|
293
293
|
export * from './reconcile.js';
|
|
294
294
|
export * from './release-pipeline.js';
|
|
295
295
|
export * from './network-policy.js';
|
|
296
|
+
export * from './retention.js';
|
|
296
297
|
export type { VerifiedImplementationReviewResult, VerifiedImplementationReviewOk, VerifiedImplementationReviewErr, ImplementationReviewRejectionReason, } from './handoff-artifacts.js';
|
|
297
298
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RetentionResult {
|
|
2
|
+
readonly freedBytes: number;
|
|
3
|
+
readonly removedPaths: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export type RetentionKind = 'transition-log' | 'graph-cache' | 'spec-store-backup' | 'idempotency' | 'snapshot' | 'spec-artifacts';
|
|
6
|
+
export interface EnforceRetentionInput {
|
|
7
|
+
readonly kind: RetentionKind;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly protectedPaths?: readonly string[];
|
|
10
|
+
readonly specId?: string;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=retention.d.ts.map
|
package/dist/types/scope.d.ts
CHANGED
|
@@ -126,4 +126,27 @@ export interface ContradictionPatternDef {
|
|
|
126
126
|
/** Recommended resolution action. */
|
|
127
127
|
recommendation: string;
|
|
128
128
|
}
|
|
129
|
+
/** A pair of antonymic direction phrases used to detect cross-spec premise drift (SPEC-1702). */
|
|
130
|
+
export interface DirectionalPhrasePair {
|
|
131
|
+
/** One side of the directional claim, e.g. "re-roots at". */
|
|
132
|
+
phraseA: string;
|
|
133
|
+
/** The opposite side of the directional claim, e.g. "relocates to". */
|
|
134
|
+
phraseB: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A contradiction between a spec's body claim about a done/approved sibling spec
|
|
138
|
+
* and that sibling's own delivered contract (SPEC-1702).
|
|
139
|
+
*/
|
|
140
|
+
export interface CrossSpecPremiseFinding {
|
|
141
|
+
/** ID of the spec containing the premise claim (e.g. "SPEC-1698"). */
|
|
142
|
+
targetSpecId: string;
|
|
143
|
+
/** ID of the referenced sibling spec whose contract contradicts the claim (e.g. "SPEC-1695"). */
|
|
144
|
+
siblingSpecId: string;
|
|
145
|
+
/** The exact sentence in the target spec asserting the contradicted claim. */
|
|
146
|
+
targetSentence: string;
|
|
147
|
+
/** The exact sentence in the sibling spec's own body stating the opposite direction. */
|
|
148
|
+
siblingSentence: string;
|
|
149
|
+
/** File path shared between both specs' ## Files sections, evidencing the same subject. */
|
|
150
|
+
sharedFilePath: string;
|
|
151
|
+
}
|
|
129
152
|
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -139,6 +139,13 @@ export interface Spec {
|
|
|
139
139
|
* Contains failure scenarios, addressed count, and overall risk.
|
|
140
140
|
*/
|
|
141
141
|
challengeReport?: ChallengeReport;
|
|
142
|
+
/**
|
|
143
|
+
* SPEC-1699: sha256 digest of the spec.md content this record's title was
|
|
144
|
+
* last synced from. Absent or mismatched against the current spec.md means
|
|
145
|
+
* the record is stale and is refreshed on the next read that supplies a
|
|
146
|
+
* canonicalRoot (see spec-store.ts refreshSpecFromDisk).
|
|
147
|
+
*/
|
|
148
|
+
contentDigest?: string;
|
|
142
149
|
}
|
|
143
150
|
/** SPEC-964: Result of running challenge_spec on a spec. */
|
|
144
151
|
export interface ChallengeReport {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** One spec's fields as parsed from its canonical spec.md, plus a content digest. */
|
|
2
|
+
export interface SpecIndexRecord {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly status: string;
|
|
6
|
+
readonly type: string;
|
|
7
|
+
readonly target: string;
|
|
8
|
+
readonly scope: string;
|
|
9
|
+
readonly difficulty: number | null;
|
|
10
|
+
readonly risk: string;
|
|
11
|
+
readonly createdAt: string | null;
|
|
12
|
+
readonly updatedAt: string;
|
|
13
|
+
readonly specPath: string;
|
|
14
|
+
readonly digest: string;
|
|
15
|
+
/** GIVEN/WHEN/THEN acceptance-criteria lines parsed from the spec.md body. */
|
|
16
|
+
readonly scenarios: readonly string[];
|
|
17
|
+
/** `TEST:` marker contents parsed from the spec.md body. */
|
|
18
|
+
readonly tests: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/** Regenerable cache persisted at planu/.runtime/index.json. spec.md is authoritative. */
|
|
21
|
+
export interface SpecIndex {
|
|
22
|
+
readonly generatedAt: string;
|
|
23
|
+
readonly specs: Readonly<Record<string, SpecIndexRecord>>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=index-cache.d.ts.map
|