@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/.planu-build.json +1 -1
  3. package/dist/cli/commands/telemetry.d.ts +3 -0
  4. package/dist/cli/commands/telemetry.js +118 -0
  5. package/dist/cli/router.js +3 -1
  6. package/dist/config/environment-schema.json +14 -0
  7. package/dist/engine/autopilot/bootstrap.js +1 -1
  8. package/dist/engine/cascade-hooks/core/append-releases.js +22 -12
  9. package/dist/engine/contradiction-detector.d.ts +2 -1
  10. package/dist/engine/contradiction-detector.js +215 -0
  11. package/dist/engine/detection-utils.d.ts +1 -0
  12. package/dist/engine/detection-utils.js +33 -0
  13. package/dist/engine/doc-generator/portal/index.d.ts +1 -1
  14. package/dist/engine/doc-generator/portal/index.js +1 -1
  15. package/dist/engine/doc-generator/portal/portal-regenerator.d.ts +8 -3
  16. package/dist/engine/doc-generator/portal/portal-regenerator.js +16 -7
  17. package/dist/engine/evidence-gates/evidence-autofill.d.ts +1 -1
  18. package/dist/engine/evidence-gates/evidence-autofill.js +1 -1
  19. package/dist/engine/framework-detector.js +8 -6
  20. package/dist/engine/handoff-artifacts/schemas.js +4 -0
  21. package/dist/engine/housekeeping/history-log.d.ts +1 -0
  22. package/dist/engine/housekeeping/history-log.js +58 -3
  23. package/dist/engine/housekeeping/index.d.ts +2 -1
  24. package/dist/engine/housekeeping/index.js +2 -1
  25. package/dist/engine/housekeeping/legacy-planu-demolisher.d.ts +3 -0
  26. package/dist/engine/housekeeping/legacy-planu-demolisher.js +164 -0
  27. package/dist/engine/housekeeping/runtime-residue-sweep.d.ts +9 -0
  28. package/dist/engine/housekeeping/runtime-residue-sweep.js +57 -0
  29. package/dist/engine/lifecycle-reconciliation.js +87 -40
  30. package/dist/engine/next-spec-resolver/orchestration-planner.js +1 -1
  31. package/dist/engine/next-spec-resolver/session-writer.js +1 -1
  32. package/dist/engine/project-graph/cache.js +23 -3
  33. package/dist/engine/readiness-checker.js +14 -2
  34. package/dist/engine/sdd-flow/checkpoints.js +29 -2
  35. package/dist/engine/session/checkpoint-writer.d.ts +1 -1
  36. package/dist/engine/session/checkpoint-writer.js +6 -5
  37. package/dist/engine/session-state/writer.js +4 -3
  38. package/dist/engine/spec-format/lean-spec-generator.js +1 -1
  39. package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +8 -1
  40. package/dist/engine/spec-migrator/planu-canonical-policy.js +14 -13
  41. package/dist/engine/spec-migrator/strict-planu-cleanup.js +28 -3
  42. package/dist/engine/telemetry/error-reporter.d.ts +9 -9
  43. package/dist/engine/telemetry/error-reporter.js +15 -34
  44. package/dist/engine/telemetry/event-envelope.d.ts +11 -0
  45. package/dist/engine/telemetry/event-envelope.js +124 -0
  46. package/dist/engine/telemetry/telemetry-client.d.ts +8 -1
  47. package/dist/engine/telemetry/telemetry-client.js +38 -20
  48. package/dist/engine/telemetry/telemetry-store.d.ts +15 -2
  49. package/dist/engine/telemetry/telemetry-store.js +73 -2
  50. package/dist/engine/universal-rules/rules/planu-release-policy.js +1 -1
  51. package/dist/engine/validator/spec-compliance-runner.d.ts +2 -1
  52. package/dist/engine/validator/spec-compliance-runner.js +123 -1
  53. package/dist/hosts/claude-code/ux/mcp-resources.js +7 -23
  54. package/dist/index.js +26 -0
  55. package/dist/resources/specs.js +12 -33
  56. package/dist/storage/current-project.d.ts +3 -0
  57. package/dist/storage/current-project.js +21 -0
  58. package/dist/storage/index.d.ts +1 -0
  59. package/dist/storage/index.js +1 -0
  60. package/dist/storage/migrations/canonical-storage.js +5 -0
  61. package/dist/storage/retention.d.ts +14 -0
  62. package/dist/storage/retention.js +279 -0
  63. package/dist/storage/spec-index.d.ts +23 -0
  64. package/dist/storage/spec-index.js +123 -0
  65. package/dist/storage/spec-store.d.ts +8 -3
  66. package/dist/storage/spec-store.js +76 -6
  67. package/dist/storage/storage-catalog.js +3 -3
  68. package/dist/storage/storage-layout.d.ts +8 -0
  69. package/dist/storage/storage-layout.js +9 -0
  70. package/dist/storage/transition-log.js +2 -0
  71. package/dist/tools/challenge-spec.js +35 -19
  72. package/dist/tools/create-spec.js +10 -0
  73. package/dist/tools/execute-sdd-flow.js +11 -2
  74. package/dist/tools/export-spec.js +2 -1
  75. package/dist/tools/force-status-analytics.js +2 -1
  76. package/dist/tools/generate-docs-site.js +2 -1
  77. package/dist/tools/generate-proposal.js +6 -2
  78. package/dist/tools/init-project/claude-md-generator.js +19 -2
  79. package/dist/tools/init-project/conventions-writer.d.ts +5 -2
  80. package/dist/tools/init-project/conventions-writer.js +18 -13
  81. package/dist/tools/init-project/git-setup.js +9 -0
  82. package/dist/tools/init-project/handler.js +9 -1
  83. package/dist/tools/init-project/legacy-planu.d.ts +2 -0
  84. package/dist/tools/init-project/legacy-planu.js +18 -0
  85. package/dist/tools/init-project/legacy-root-migration.d.ts +15 -0
  86. package/dist/tools/init-project/legacy-root-migration.js +213 -0
  87. package/dist/tools/init-project/runtime-residue.d.ts +2 -0
  88. package/dist/tools/init-project/runtime-residue.js +11 -0
  89. package/dist/tools/init-project/schedule-housekeeping.d.ts +2 -0
  90. package/dist/tools/init-project/schedule-housekeeping.js +8 -0
  91. package/dist/tools/reconcile-spec.js +29 -2
  92. package/dist/tools/register-spec-tools/analysis-tools.d.ts +7 -0
  93. package/dist/tools/register-spec-tools/analysis-tools.js +13 -1
  94. package/dist/tools/safe-handler.js +6 -12
  95. package/dist/tools/session-checkpoint.js +1 -1
  96. package/dist/tools/update-status/index.js +7 -1
  97. package/dist/tools/update-status-actions.d.ts +7 -1
  98. package/dist/tools/update-status-actions.js +10 -2
  99. package/dist/types/handoff-artifacts.d.ts +1 -0
  100. package/dist/types/housekeeping.d.ts +38 -0
  101. package/dist/types/housekeeping.js +0 -1
  102. package/dist/types/index.d.ts +1 -0
  103. package/dist/types/index.js +1 -0
  104. package/dist/types/retention.d.ts +12 -0
  105. package/dist/types/retention.js +3 -0
  106. package/dist/types/scope.d.ts +23 -0
  107. package/dist/types/spec/core.d.ts +7 -0
  108. package/dist/types/spec/index-cache.d.ts +25 -0
  109. package/dist/types/spec/index-cache.js +3 -0
  110. package/dist/types/spec/index.d.ts +1 -0
  111. package/dist/types/spec/index.js +1 -0
  112. package/dist/types/spec/inputs.d.ts +9 -0
  113. package/dist/types/spec-format.d.ts +1 -0
  114. package/dist/types/telemetry.d.ts +39 -1
  115. package/dist/types/validation-evidence.d.ts +6 -0
  116. package/package.json +1 -1
  117. package/planu-plugin.json +1 -1
  118. 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,2 @@
1
+ export declare function scheduleRuntimeResidueSweep(projectPath: string): void;
2
+ //# sourceMappingURL=runtime-residue.d.ts.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,2 @@
1
+ export declare function scheduleHousekeepingSweeps(projectPath: string): void;
2
+ //# sourceMappingURL=schedule-housekeeping.d.ts.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 { sendTelemetryEvent } from '../engine/telemetry/telemetry-client.js';
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 tool_used telemetry for successful calls (fire-and-forget)
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
- sendTelemetryEvent({
516
- event: 'tool_used',
517
- properties: {
518
- tool: toolName,
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
- 'releases/pending.json: refresh queued',
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
@@ -1,3 +1,2 @@
1
- // types/housekeeping.ts — SPEC-751: Housekeeping sweep types
2
1
  export {};
3
2
  //# sourceMappingURL=housekeeping.js.map
@@ -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
@@ -304,4 +304,5 @@ export * from './reconcile.js';
304
304
  // SPEC-1012: Release spec closer types
305
305
  export * from './release-pipeline.js';
306
306
  export * from './network-policy.js';
307
+ export * from './retention.js';
307
308
  //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1,3 @@
1
+ // types/retention.ts — SPEC-1697: retention budgets for runtime storage artifacts
2
+ export {};
3
+ //# sourceMappingURL=retention.js.map
@@ -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
@@ -0,0 +1,3 @@
1
+ // Planu — Regenerable spec.md read-index types (SPEC-1699)
2
+ export {};
3
+ //# sourceMappingURL=index-cache.js.map