@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
|
@@ -29,8 +29,11 @@ export declare function invalidateSpecsCache(projectId: string): void;
|
|
|
29
29
|
export declare function discoverCanonicalSpecPath(specId: string, canonicalRoot: string): Promise<string | null>;
|
|
30
30
|
/**
|
|
31
31
|
* List every spec for a project.
|
|
32
|
+
*
|
|
33
|
+
* When `canonicalRoot` is supplied, every record's content-derived fields are
|
|
34
|
+
* verified against the current spec.md before being returned (SPEC-1699).
|
|
32
35
|
*/
|
|
33
|
-
export declare function listSpecs(projectId: string): Promise<Spec[]>;
|
|
36
|
+
export declare function listSpecs(projectId: string, canonicalRoot?: string): Promise<Spec[]>;
|
|
34
37
|
/**
|
|
35
38
|
* Run a read-modify side effect against a fresh spec snapshot while holding the
|
|
36
39
|
* same cross-process lock used by status mutations.
|
|
@@ -39,9 +42,11 @@ export declare function withFreshSpecsLock<T>(projectId: string, fn: (specs: Spe
|
|
|
39
42
|
/**
|
|
40
43
|
* Get a single spec by ID. Returns `null` when not found.
|
|
41
44
|
*
|
|
42
|
-
* When `canonicalRoot` is supplied
|
|
45
|
+
* When `canonicalRoot` is supplied: a legacy absolute `specPath`/`technicalPath`
|
|
43
46
|
* for this spec is migrated to its portable project-relative identity first
|
|
44
|
-
* (best-effort — a failed migration attempt never blocks the read)
|
|
47
|
+
* (best-effort — a failed migration attempt never blocks the read), and the
|
|
48
|
+
* returned record's content-derived fields are verified against the current
|
|
49
|
+
* spec.md before being returned (SPEC-1699).
|
|
45
50
|
*/
|
|
46
51
|
export declare function getSpec(projectId: string, specId: string, canonicalRoot?: string): Promise<Spec | null>;
|
|
47
52
|
/** Result of a {@link migrateLegacySpecPaths} run. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { reportClassifiedDegradation } from '../errors/classified-degradation.js';
|
|
2
2
|
import { CriticalJsonReadError, readCriticalJson, readJson, writeJson, projectDataDir, } from './base-store.js';
|
|
3
|
+
import { enforceRetention } from './retention.js';
|
|
3
4
|
import { decodeCriticalSchema, registerCriticalSchema } from './schema-registry.js';
|
|
4
5
|
import { BoundaryFailure } from '../errors/error-taxonomy.js';
|
|
5
6
|
import { withFileLock } from './file-mutex.js';
|
|
@@ -8,11 +9,13 @@ import { computeHealthScore } from '../engine/spec-health-scorer.js';
|
|
|
8
9
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
9
10
|
import { loadSpecContent } from '../engine/validation-loop.js';
|
|
10
11
|
import { createHash, randomUUID } from 'node:crypto';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
11
13
|
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
12
14
|
import { glob } from 'glob';
|
|
13
15
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
14
16
|
import { acquireLock, releaseLock } from '../engine/safety/cross-process-lock.js';
|
|
15
|
-
import { resolvePortableSpecPath, toPortableSpecPath, PortablePathError, } from './project-identity.js';
|
|
17
|
+
import { resolvePortableSpecPath, resolveVerifiedSpecPath, toPortableSpecPath, PortablePathError, } from './project-identity.js';
|
|
18
|
+
import { digestOf, readFrontmatterScalars } from './spec-index.js';
|
|
16
19
|
import { appendEntry, getLastHash } from './audit-trail-store.js';
|
|
17
20
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
18
21
|
import { hashEntry } from '../engine/audit-trail/hasher.js';
|
|
@@ -200,6 +203,7 @@ async function saveAll(projectId, specs) {
|
|
|
200
203
|
const normalized = specs.map((spec) => normalizeSpec(spec));
|
|
201
204
|
validateCanonicalSpecRows(projectId, normalized);
|
|
202
205
|
await writeJson(specsFile(projectId), { schemaVersion: 1, data: normalized });
|
|
206
|
+
await enforceRetention({ kind: 'spec-store-backup', path: specsFile(projectId) });
|
|
203
207
|
specsCache.set(projectId, normalized);
|
|
204
208
|
}
|
|
205
209
|
/** Evict the cache for a project (e.g. after external file changes). */
|
|
@@ -221,12 +225,72 @@ export async function discoverCanonicalSpecPath(specId, canonicalRoot) {
|
|
|
221
225
|
});
|
|
222
226
|
return candidates.length === 1 ? (candidates[0] ?? null) : null;
|
|
223
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Persist a digest-refresh in isolation from whatever mutation the caller of
|
|
230
|
+
* getSpec/listSpecs is otherwise doing. Best-effort: failure to persist never
|
|
231
|
+
* blocks the caller — the freshly parsed value is already what gets returned.
|
|
232
|
+
*/
|
|
233
|
+
async function persistDigestRefresh(projectId, specId, title, contentDigest) {
|
|
234
|
+
await withSpecMutationLock(projectId, async () => {
|
|
235
|
+
const specs = await loadAll(projectId);
|
|
236
|
+
const idx = specs.findIndex((s) => s.id === specId);
|
|
237
|
+
if (idx === -1) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const nextSpecs = specs.map((current, currentIdx) => currentIdx === idx ? { ...current, title, contentDigest } : current);
|
|
241
|
+
await saveAll(projectId, nextSpecs);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* SPEC-1699: single-source-of-truth choke point. spec.md is authoritative for
|
|
246
|
+
* the fields it carries — before returning a record, verify the persisted
|
|
247
|
+
* `contentDigest` still matches the current spec.md on disk. On a mismatch or
|
|
248
|
+
* missing digest, re-parse spec.md and return the fresh title/digest instead
|
|
249
|
+
* of the possibly-stale specs.json copy. Requires `canonicalRoot` to locate
|
|
250
|
+
* spec.md; callers that omit it get the specs.json record unchecked (matching
|
|
251
|
+
* the existing canonicalRoot-gated legacy-migration behavior on this same path).
|
|
252
|
+
*/
|
|
253
|
+
async function refreshSpecFromDisk(projectId, spec, canonicalRoot) {
|
|
254
|
+
let specMdPath;
|
|
255
|
+
try {
|
|
256
|
+
specMdPath = await resolveVerifiedSpecPath(spec.id, spec.specPath, canonicalRoot);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
reportClassifiedDegradation('SPEC_CONTENT_DIGEST_CHECK_SKIPPED', error);
|
|
260
|
+
return spec;
|
|
261
|
+
}
|
|
262
|
+
let content;
|
|
263
|
+
try {
|
|
264
|
+
content = await readFile(specMdPath, 'utf-8');
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
reportClassifiedDegradation('SPEC_CONTENT_DIGEST_CHECK_SKIPPED', error);
|
|
268
|
+
return spec;
|
|
269
|
+
}
|
|
270
|
+
const digest = digestOf(content);
|
|
271
|
+
if (digest === spec.contentDigest) {
|
|
272
|
+
return spec;
|
|
273
|
+
}
|
|
274
|
+
const fields = readFrontmatterScalars(content);
|
|
275
|
+
const refreshed = { ...spec, title: fields.title ?? spec.title, contentDigest: digest };
|
|
276
|
+
await persistDigestRefresh(projectId, spec.id, refreshed.title, digest).catch((error) => {
|
|
277
|
+
reportClassifiedDegradation('SPEC_CONTENT_DIGEST_REFRESH_PERSIST', error);
|
|
278
|
+
});
|
|
279
|
+
return refreshed;
|
|
280
|
+
}
|
|
224
281
|
// --- public API ---
|
|
225
282
|
/**
|
|
226
283
|
* List every spec for a project.
|
|
284
|
+
*
|
|
285
|
+
* When `canonicalRoot` is supplied, every record's content-derived fields are
|
|
286
|
+
* verified against the current spec.md before being returned (SPEC-1699).
|
|
227
287
|
*/
|
|
228
|
-
export async function listSpecs(projectId) {
|
|
229
|
-
|
|
288
|
+
export async function listSpecs(projectId, canonicalRoot) {
|
|
289
|
+
const specs = await loadAll(projectId);
|
|
290
|
+
if (canonicalRoot === undefined) {
|
|
291
|
+
return specs;
|
|
292
|
+
}
|
|
293
|
+
return Promise.all(specs.map((spec) => refreshSpecFromDisk(projectId, spec, canonicalRoot)));
|
|
230
294
|
}
|
|
231
295
|
/**
|
|
232
296
|
* Run a read-modify side effect against a fresh spec snapshot while holding the
|
|
@@ -238,9 +302,11 @@ export async function withFreshSpecsLock(projectId, fn) {
|
|
|
238
302
|
/**
|
|
239
303
|
* Get a single spec by ID. Returns `null` when not found.
|
|
240
304
|
*
|
|
241
|
-
* When `canonicalRoot` is supplied
|
|
305
|
+
* When `canonicalRoot` is supplied: a legacy absolute `specPath`/`technicalPath`
|
|
242
306
|
* for this spec is migrated to its portable project-relative identity first
|
|
243
|
-
* (best-effort — a failed migration attempt never blocks the read)
|
|
307
|
+
* (best-effort — a failed migration attempt never blocks the read), and the
|
|
308
|
+
* returned record's content-derived fields are verified against the current
|
|
309
|
+
* spec.md before being returned (SPEC-1699).
|
|
244
310
|
*/
|
|
245
311
|
export async function getSpec(projectId, specId, canonicalRoot) {
|
|
246
312
|
if (canonicalRoot !== undefined) {
|
|
@@ -249,7 +315,11 @@ export async function getSpec(projectId, specId, canonicalRoot) {
|
|
|
249
315
|
});
|
|
250
316
|
}
|
|
251
317
|
const specs = await loadAll(projectId);
|
|
252
|
-
|
|
318
|
+
const spec = specs.find((s) => s.id === specId) ?? null;
|
|
319
|
+
if (spec && canonicalRoot !== undefined) {
|
|
320
|
+
return refreshSpecFromDisk(projectId, spec, canonicalRoot);
|
|
321
|
+
}
|
|
322
|
+
return spec;
|
|
253
323
|
}
|
|
254
324
|
/**
|
|
255
325
|
* Record one hash-chained audit event for a legacy specPath migration batch.
|
|
@@ -69,9 +69,9 @@ export function createStorageCatalog(context = {}) {
|
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
id: 'portable-release-evidence',
|
|
72
|
-
templatePath: '<project>/planu/releases
|
|
73
|
-
resolvedPath: portableRoot ? join(portableRoot, '
|
|
74
|
-
purpose: '
|
|
72
|
+
templatePath: '<project>/planu/project.json#releases',
|
|
73
|
+
resolvedPath: portableRoot ? join(portableRoot, 'project.json') : undefined,
|
|
74
|
+
purpose: 'Pending release declarations tracked in the releases field of project.json.',
|
|
75
75
|
owner: 'project',
|
|
76
76
|
sourceOfTruth: true,
|
|
77
77
|
lifecycle: 'portable',
|
|
@@ -19,6 +19,14 @@ export interface StorageLayoutOptions {
|
|
|
19
19
|
readonly homeDirectory?: string;
|
|
20
20
|
readonly platform?: NodeJS.Platform;
|
|
21
21
|
}
|
|
22
|
+
export type ProjectRuntimeSubdir = 'state' | 'cache' | 'logs' | 'reports';
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the per-project runtime directory for a subdir kind.
|
|
25
|
+
* Rooted at <projectPath>/planu/.runtime/ — the only per-project runtime root.
|
|
26
|
+
* Platform storage roots from resolveStorageLayout are machine-local and do not
|
|
27
|
+
* apply to per-project artifacts.
|
|
28
|
+
*/
|
|
29
|
+
export declare function projectRuntimeDir(projectPath: string, subdir: ProjectRuntimeSubdir): string;
|
|
22
30
|
/** Resolve every machine-local Planu path independently from the current directory. */
|
|
23
31
|
export declare function resolveStorageLayout(options?: StorageLayoutOptions): StorageLayout;
|
|
24
32
|
//# sourceMappingURL=storage-layout.d.ts.map
|
|
@@ -73,6 +73,15 @@ function resolvePlatformRoots(platform, home, env) {
|
|
|
73
73
|
}
|
|
74
74
|
return xdgRoots(home, env);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the per-project runtime directory for a subdir kind.
|
|
78
|
+
* Rooted at <projectPath>/planu/.runtime/ — the only per-project runtime root.
|
|
79
|
+
* Platform storage roots from resolveStorageLayout are machine-local and do not
|
|
80
|
+
* apply to per-project artifacts.
|
|
81
|
+
*/
|
|
82
|
+
export function projectRuntimeDir(projectPath, subdir) {
|
|
83
|
+
return join(projectPath, 'planu', '.runtime', subdir);
|
|
84
|
+
}
|
|
76
85
|
/** Resolve every machine-local Planu path independently from the current directory. */
|
|
77
86
|
export function resolveStorageLayout(options = {}) {
|
|
78
87
|
const env = options.env ?? process.env;
|
|
@@ -9,6 +9,7 @@ import { appendFile, mkdir } from 'node:fs/promises';
|
|
|
9
9
|
import { createReadStream } from 'node:fs';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
11
|
import { projectDataDir } from './base-store.js';
|
|
12
|
+
import { enforceRetention } from './retention.js';
|
|
12
13
|
import { reportClassifiedDegradation } from '../errors/classified-degradation.js';
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Path helper
|
|
@@ -56,6 +57,7 @@ function computeEntrySha(entry) {
|
|
|
56
57
|
const writeQueues = new Map();
|
|
57
58
|
async function doAppend(filePath, input) {
|
|
58
59
|
await mkdir(dirname(filePath), { recursive: true });
|
|
60
|
+
await enforceRetention({ kind: 'transition-log', path: filePath });
|
|
59
61
|
let lastLine;
|
|
60
62
|
for await (const line of streamJsonlLines(filePath)) {
|
|
61
63
|
lastLine = line;
|
|
@@ -15,6 +15,7 @@ import { t, ti } from '../i18n/index.js';
|
|
|
15
15
|
import { generateConcurrencyAnalysis, buildScalabilityAssessment, calculateOverallRisk, readSpecContent, } from './challenge-spec-helpers.js';
|
|
16
16
|
import { prioritizeScenarios, buildPrioritizedSummary } from '../engine/challenge-prioritizer.js';
|
|
17
17
|
import { checkContradictions as checkScopeContradictions } from '../engine/scope-boundaries/index.js';
|
|
18
|
+
import { detectCrossSpecPremiseContradictions } from '../engine/contradiction-detector.js';
|
|
18
19
|
import { buildChallengeSpecSummary } from '../engine/human-summary.js';
|
|
19
20
|
import { detectContradictions as detectDecisionContradictions, searchPriorDecisions, } from '../engine/prior-decisions/index.js';
|
|
20
21
|
import { getDecisions } from '../storage/decision-store.js';
|
|
@@ -85,25 +86,23 @@ function extractOutOfScopeSection(content) {
|
|
|
85
86
|
}
|
|
86
87
|
return found ? captured.join('\n') : null;
|
|
87
88
|
}
|
|
88
|
-
async function
|
|
89
|
+
async function readRawSpecMarkdown(spec, projectPath) {
|
|
89
90
|
if (!spec.specPath) {
|
|
90
|
-
return
|
|
91
|
+
return null;
|
|
91
92
|
}
|
|
92
|
-
let fileContent;
|
|
93
93
|
try {
|
|
94
94
|
const resolvedPath = !isAbsolute(spec.specPath) && projectPath
|
|
95
95
|
? await resolveVerifiedSpecPath(spec.id, spec.specPath, projectPath)
|
|
96
96
|
: spec.specPath;
|
|
97
|
-
|
|
97
|
+
return await readFile(resolvedPath, 'utf-8');
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
|
-
return
|
|
100
|
+
return null;
|
|
101
101
|
}
|
|
102
|
-
const sectionContent = extractOutOfScopeSection(fileContent);
|
|
103
|
-
return sectionContent === null ? [] : extractListItems(sectionContent);
|
|
104
102
|
}
|
|
105
|
-
|
|
106
|
-
const
|
|
103
|
+
function resolveOutOfScopeItems(spec, rawSpecMarkdown) {
|
|
104
|
+
const sectionContent = rawSpecMarkdown === null ? null : extractOutOfScopeSection(rawSpecMarkdown);
|
|
105
|
+
const documentItems = sectionContent === null ? [] : extractListItems(sectionContent);
|
|
107
106
|
return documentItems.length > 0 ? documentItems : (spec.outOfScope ?? []);
|
|
108
107
|
}
|
|
109
108
|
/**
|
|
@@ -172,15 +171,7 @@ export async function handleChallengeSpec(args, server) {
|
|
|
172
171
|
if (!projectId) {
|
|
173
172
|
return missingProjectIdError;
|
|
174
173
|
}
|
|
175
|
-
// 1. Load
|
|
176
|
-
const spec = await specStore.getSpec(projectId, specId);
|
|
177
|
-
if (!spec) {
|
|
178
|
-
return {
|
|
179
|
-
content: [{ type: 'text', text: ti('spec.notFound', { id: specId }) }],
|
|
180
|
-
isError: true,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// 2. Load project knowledge — SPEC-1011 Bug F: use disk fingerprint fallback
|
|
174
|
+
// 1. Load project knowledge — SPEC-1011 Bug F: use disk fingerprint fallback
|
|
184
175
|
let knowledge = await knowledgeStore.getKnowledge(projectId);
|
|
185
176
|
if (!knowledge && args.projectPath) {
|
|
186
177
|
const resolved = await resolveProjectFromPath(args.projectPath);
|
|
@@ -194,6 +185,15 @@ export async function handleChallengeSpec(args, server) {
|
|
|
194
185
|
isError: true,
|
|
195
186
|
};
|
|
196
187
|
}
|
|
188
|
+
// 2. Load spec — SPEC-1699: pass canonicalRoot so a stale specs.json record
|
|
189
|
+
// (e.g. spec.md hand-edited after the last write) is refreshed on read.
|
|
190
|
+
const spec = await specStore.getSpec(projectId, specId, knowledge.projectPath);
|
|
191
|
+
if (!spec) {
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: 'text', text: ti('spec.notFound', { id: specId }) }],
|
|
194
|
+
isError: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
197
|
// 2b. Load Constitution for compliance context
|
|
198
198
|
const constitution = await knowledgeStore.getConstitution(projectId);
|
|
199
199
|
// 3. Read spec content
|
|
@@ -229,7 +229,8 @@ export async function handleChallengeSpec(args, server) {
|
|
|
229
229
|
}
|
|
230
230
|
failureScenarios.push(...collectCapabilityScenarios({ spec, specContent, knowledge, focusAreas, capabilities }));
|
|
231
231
|
// SPEC-612: Check for contradictions between outOfScope and acceptance criteria
|
|
232
|
-
const
|
|
232
|
+
const rawSpecMarkdown = await readRawSpecMarkdown(spec, knowledge.projectPath);
|
|
233
|
+
const resolvedOutOfScope = resolveOutOfScopeItems(spec, rawSpecMarkdown);
|
|
233
234
|
if (resolvedOutOfScope.length > 0) {
|
|
234
235
|
const criteriaRange = findMarkdownSectionRange(specContent, 'Acceptance Criteria');
|
|
235
236
|
const criteriaTexts = criteriaRange === null
|
|
@@ -248,6 +249,21 @@ export async function handleChallengeSpec(args, server) {
|
|
|
248
249
|
});
|
|
249
250
|
}
|
|
250
251
|
}
|
|
252
|
+
// SPEC-1702: Check for premise contradictions against done/approved sibling specs
|
|
253
|
+
const crossSpecPremiseFindings = rawSpecMarkdown !== null
|
|
254
|
+
? await detectCrossSpecPremiseContradictions(spec.id, rawSpecMarkdown, knowledge.projectPath)
|
|
255
|
+
: [];
|
|
256
|
+
for (const finding of crossSpecPremiseFindings) {
|
|
257
|
+
failureScenarios.push({
|
|
258
|
+
scenario: `cross-spec-premise: ${finding.targetSpecId} contradicts ${finding.siblingSpecId}`,
|
|
259
|
+
probability: 'high',
|
|
260
|
+
impact: 'critical',
|
|
261
|
+
currentHandling: `${finding.targetSpecId} claims: "${finding.targetSentence}"`,
|
|
262
|
+
requiredHandling: `${finding.siblingSpecId} states: "${finding.siblingSentence}" — reconcile the premise before implementation (shared file: ${finding.sharedFilePath})`,
|
|
263
|
+
dataConsistency: 'Cross-spec premise drift',
|
|
264
|
+
userExperience: 'Implementing on a contradicted premise risks a wasted implementation round',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
251
267
|
// SPEC-615 AC3: Check criteria against prior decisions for technology contradictions
|
|
252
268
|
await runPriorDecisionContradictions(projectId, spec.title, spec.tags, specContent, failureScenarios);
|
|
253
269
|
// 6. Generate concurrency analysis
|
|
@@ -2,6 +2,7 @@ import { checkGate } from '../engine/clarification-gate/gate.js';
|
|
|
2
2
|
import { upsertToken, hashQuestions } from '../engine/clarification-gate/token-store.js';
|
|
3
3
|
import { ti } from '../i18n/index.js';
|
|
4
4
|
import { knowledgeStore, specStore } from '../storage/index.js';
|
|
5
|
+
import { loadSpecIndex } from '../storage/spec-index.js';
|
|
5
6
|
import { readTechnologySelectionContract } from '../storage/technology-selection-store.js';
|
|
6
7
|
import { toolResult, interactiveResult } from './response-helpers.js';
|
|
7
8
|
import { readFile, stat as fsStat } from 'node:fs/promises';
|
|
@@ -9,6 +10,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
9
10
|
import { pathExistsByStat as pathExists } from '../core/shared/fs.js';
|
|
10
11
|
import { basename as pathBasename, dirname as pathDirname, isAbsolute as pathIsAbsolute, join as pathJoin, relative as pathRelative, sep as pathSeparator, } from 'node:path';
|
|
11
12
|
import { withFileLock } from '../storage/file-mutex.js';
|
|
13
|
+
import { enforceRetention } from '../storage/retention.js';
|
|
12
14
|
import { generateBranchName } from './create-spec-helpers.js';
|
|
13
15
|
import { buildSpecContext, buildSplitResult, computeNextSpecId, } from './create-spec/spec-builder.js';
|
|
14
16
|
import { validateConstitution } from './create-spec/constitution-validator.js';
|
|
@@ -635,6 +637,7 @@ async function commitIdempotencyEvidence(projectPath, key, claim, spec, specPath
|
|
|
635
637
|
await executionWriteFileExclusive(evidencePath, JSON.stringify(evidence, null, 2), {
|
|
636
638
|
encoding: 'utf-8',
|
|
637
639
|
});
|
|
640
|
+
await enforceRetention({ kind: 'idempotency', path: pathDirname(evidencePath) });
|
|
638
641
|
}
|
|
639
642
|
catch (error) {
|
|
640
643
|
if (error.code !== 'EEXIST') {
|
|
@@ -1688,6 +1691,13 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1688
1691
|
}
|
|
1689
1692
|
await measureStep('specStore-createSpec', () => specStore.createSpec(projectId, spec));
|
|
1690
1693
|
storeCreated = true;
|
|
1694
|
+
// SPEC-1699: refresh the regenerable spec.md index cache file.
|
|
1695
|
+
// Best-effort — specStore.getSpec/listSpecs independently verify
|
|
1696
|
+
// spec.md's digest on every read that supplies canonicalRoot, so
|
|
1697
|
+
// a failed refresh here only lags this cache file, not a caller.
|
|
1698
|
+
await loadSpecIndex(resolvedPath).catch(() => {
|
|
1699
|
+
/* best-effort */
|
|
1700
|
+
});
|
|
1691
1701
|
assertExecutionCanCommit(criticalSignal);
|
|
1692
1702
|
await measureStep('commit-idempotency-evidence', () => commitIdempotencyEvidence(resolvedPath, idempotencyKey, idempotencyClaim, spec, specPath));
|
|
1693
1703
|
operationJournal.commitWithOutbox('create_spec', operationKey, committedResult, {
|
|
@@ -48,9 +48,18 @@ export async function handleExecuteSddFlow(args) {
|
|
|
48
48
|
if (specIds.length === 0 && !args.resumeFrom) {
|
|
49
49
|
return compactError('specIds is required when not resuming. Provide at least one approved spec ID.');
|
|
50
50
|
}
|
|
51
|
-
let state =
|
|
51
|
+
let state = null;
|
|
52
|
+
if (args.resumeFrom) {
|
|
53
|
+
try {
|
|
54
|
+
state = await loadCheckpoint(projectPath, args.resumeFrom);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return compactError(error instanceof Error ? error.message : String(error));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
52
60
|
if (args.resumeFrom && !state) {
|
|
53
|
-
return compactError(`Run state not found for runId: ${args.resumeFrom}.
|
|
61
|
+
return compactError(`Run state not found for runId: ${args.resumeFrom}. ` +
|
|
62
|
+
'Checked planu/.runtime/state/ and legacy planu/state/.');
|
|
54
63
|
}
|
|
55
64
|
if (!state) {
|
|
56
65
|
const runId = randomUUID();
|
|
@@ -8,6 +8,7 @@ import { stripFrontmatter } from '../engine/frontmatter-parser.js';
|
|
|
8
8
|
import { specToAgentReady, serializeAgentReady } from '../engine/agent-ready-exporter.js';
|
|
9
9
|
import { assertPathWithinBase } from '../engine/safety/assert-within-project.js';
|
|
10
10
|
import { compactResult, formatKeyValue } from './output-formatter.js';
|
|
11
|
+
import { projectRuntimeDir } from '../storage/storage-layout.js';
|
|
11
12
|
async function readFileContent(filePath) {
|
|
12
13
|
try {
|
|
13
14
|
const raw = await readFile(filePath, 'utf-8');
|
|
@@ -122,7 +123,7 @@ export async function handleExportSpec(params) {
|
|
|
122
123
|
});
|
|
123
124
|
// Write to output directory
|
|
124
125
|
const slug = toSlug(spec.title);
|
|
125
|
-
const outputDir = params.outputDir ?? join(params.projectPath, '
|
|
126
|
+
const outputDir = params.outputDir ?? join(projectRuntimeDir(params.projectPath, 'reports'), 'exports', slug);
|
|
126
127
|
let resolvedOutput;
|
|
127
128
|
try {
|
|
128
129
|
resolvedOutput = assertPathWithinBase(params.projectPath, outputDir);
|
|
@@ -4,6 +4,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { readForceAnalytics } from '../storage/force-analytics-store.js';
|
|
5
5
|
import { resolveProjectPath } from '../storage/path-resolver.js';
|
|
6
6
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
7
|
+
import { projectRuntimeDir } from '../storage/storage-layout.js';
|
|
7
8
|
function buildMarkdownReport(projectPath, entries, stats) {
|
|
8
9
|
const lines = [
|
|
9
10
|
'# Quality Exceptions Report',
|
|
@@ -42,7 +43,7 @@ export async function handleForceStatusAnalytics(params) {
|
|
|
42
43
|
let reportPath = null;
|
|
43
44
|
if (generateReport) {
|
|
44
45
|
try {
|
|
45
|
-
const reportDir =
|
|
46
|
+
const reportDir = projectRuntimeDir(projectPath, 'reports');
|
|
46
47
|
await mkdir(reportDir, { recursive: true });
|
|
47
48
|
reportPath = join(reportDir, 'quality-exceptions.md');
|
|
48
49
|
const markdown = buildMarkdownReport(projectPath, analytics.forcedTransitions, analytics.stats);
|
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { rm, access } from 'node:fs/promises';
|
|
3
3
|
import { generateDocsSite } from '../engine/docs-site-generator/index.js';
|
|
4
4
|
import { compactResult } from './output-formatter.js';
|
|
5
|
+
import { projectRuntimeDir } from '../storage/storage-layout.js';
|
|
5
6
|
async function removeMisplacedDocsSite(projectPath) {
|
|
6
7
|
const stale = join(projectPath, 'docs-site');
|
|
7
8
|
try {
|
|
@@ -17,7 +18,7 @@ export async function handleGenerateDocsSite(params) {
|
|
|
17
18
|
const removed = await removeMisplacedDocsSite(params.projectPath);
|
|
18
19
|
const config = {
|
|
19
20
|
projectPath: params.projectPath,
|
|
20
|
-
outputDir: join(params.projectPath, '
|
|
21
|
+
outputDir: join(projectRuntimeDir(params.projectPath, 'cache'), 'docs-site'),
|
|
21
22
|
title: params.title,
|
|
22
23
|
};
|
|
23
24
|
const result = await generateDocsSite(config);
|
|
@@ -6,6 +6,7 @@ import { hashProjectPath, projectDataDir } from '../storage/base-store.js';
|
|
|
6
6
|
import { specStore, knowledgeStore, decisionStore, metricsStore } from '../storage/index.js';
|
|
7
7
|
import { loadAllRiskRegisters } from '../storage/risk-store.js';
|
|
8
8
|
import { generateProposal } from '../engine/doc-generator/proposal/proposal-generator.js';
|
|
9
|
+
import { projectRuntimeDir } from '../storage/storage-layout.js';
|
|
9
10
|
import { SpecIdSchema } from './schemas/index.js';
|
|
10
11
|
// ── Zod schema ────────────────────────────────────────────────────────────────
|
|
11
12
|
const PhaseSchema = z.object({
|
|
@@ -131,8 +132,11 @@ async function handleGenerateProposal(args) {
|
|
|
131
132
|
};
|
|
132
133
|
const html = generateProposal(proposalData);
|
|
133
134
|
// Resolve output path
|
|
134
|
-
const
|
|
135
|
-
|
|
135
|
+
const outputFilePath = args.outputPath
|
|
136
|
+
? isAbsolute(args.outputPath)
|
|
137
|
+
? args.outputPath
|
|
138
|
+
: join(args.projectPath, args.outputPath)
|
|
139
|
+
: join(projectRuntimeDir(args.projectPath, 'reports'), 'proposal.html');
|
|
136
140
|
await mkdir(dirname(outputFilePath), { recursive: true });
|
|
137
141
|
await writeFile(outputFilePath, html, 'utf-8');
|
|
138
142
|
const sizeKb = Math.round(html.length / 1024);
|
|
@@ -116,7 +116,7 @@ function buildArchitecture(knowledge) {
|
|
|
116
116
|
} // only monorepo=No is not useful alone
|
|
117
117
|
return wrap('architecture', `## Architecture\n${items.join('\n')}`);
|
|
118
118
|
}
|
|
119
|
-
function
|
|
119
|
+
function detectTestFrameworkFromCommand(testCommand) {
|
|
120
120
|
if (testCommand.includes(technologyValue('technology-vitest-a9127f'))) {
|
|
121
121
|
return technologyValue('technology-vitest-7fc39e');
|
|
122
122
|
}
|
|
@@ -143,11 +143,28 @@ function detectTestFramework(testCommand) {
|
|
|
143
143
|
}
|
|
144
144
|
return 'Unknown';
|
|
145
145
|
}
|
|
146
|
+
const TOOLING_TEST_FRAMEWORK_LABELS = {
|
|
147
|
+
[technologyValue('technology-vitest-a9127f')]: technologyValue('technology-vitest-7fc39e'),
|
|
148
|
+
[technologyValue('technology-jest-35e99f')]: technologyValue('technology-jest-1f30f0'),
|
|
149
|
+
mocha: 'Mocha',
|
|
150
|
+
[technologyValue('technology-pytest-2b0121')]: technologyValue('technology-pytest-2b0121'),
|
|
151
|
+
'cargo-test': 'cargo test',
|
|
152
|
+
};
|
|
153
|
+
function detectTestFramework(knowledge, testCommand) {
|
|
154
|
+
const toolingTesting = knowledge.tooling.testing;
|
|
155
|
+
if (toolingTesting) {
|
|
156
|
+
const label = TOOLING_TEST_FRAMEWORK_LABELS[toolingTesting];
|
|
157
|
+
if (label) {
|
|
158
|
+
return label;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return detectTestFrameworkFromCommand(testCommand);
|
|
162
|
+
}
|
|
146
163
|
function buildTesting(knowledge) {
|
|
147
164
|
if (!knowledge.testCommand) {
|
|
148
165
|
return null;
|
|
149
166
|
}
|
|
150
|
-
const framework = detectTestFramework(knowledge.testCommand);
|
|
167
|
+
const framework = detectTestFramework(knowledge, knowledge.testCommand);
|
|
151
168
|
const items = [
|
|
152
169
|
`- **Framework**: ${framework}`,
|
|
153
170
|
`- **Command**: \`${knowledge.testCommand}\``,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ProjectKnowledge } from '../../types/index.js';
|
|
2
|
+
export type ConventionsSource = Pick<ProjectKnowledge, 'stack' | 'language' | 'framework' | 'projectPath'>;
|
|
2
3
|
/**
|
|
3
4
|
* SPEC-530: Write .claude/rules/conventions.md if missing.
|
|
4
5
|
* Generates project-specific conventions from detected knowledge.
|
|
5
|
-
*
|
|
6
|
+
* SPEC-1699: pass force=true to regenerate even when the file already exists,
|
|
7
|
+
* e.g. after migrateLegacyPlanuRoot folds conventions.json into project.json.
|
|
8
|
+
* @returns true if the file was written, false if it already existed and force was false.
|
|
6
9
|
*/
|
|
7
|
-
export declare function generateConventionsMdIfMissing(projectPath: string, knowledge:
|
|
10
|
+
export declare function generateConventionsMdIfMissing(projectPath: string, knowledge: ConventionsSource, force?: boolean): Promise<boolean>;
|
|
8
11
|
//# sourceMappingURL=conventions-writer.d.ts.map
|
|
@@ -33,21 +33,26 @@ function buildConventionsContent(knowledge) {
|
|
|
33
33
|
/**
|
|
34
34
|
* SPEC-530: Write .claude/rules/conventions.md if missing.
|
|
35
35
|
* Generates project-specific conventions from detected knowledge.
|
|
36
|
-
*
|
|
36
|
+
* SPEC-1699: pass force=true to regenerate even when the file already exists,
|
|
37
|
+
* e.g. after migrateLegacyPlanuRoot folds conventions.json into project.json.
|
|
38
|
+
* @returns true if the file was written, false if it already existed and force was false.
|
|
37
39
|
*/
|
|
38
|
-
export async function generateConventionsMdIfMissing(projectPath, knowledge) {
|
|
40
|
+
export async function generateConventionsMdIfMissing(projectPath, knowledge, force = false) {
|
|
39
41
|
const conventionsPath = join(projectPath, CONVENTIONS_MD_PATH);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assertEnglishOnlyArtifactText(content, 'rule');
|
|
49
|
-
await writeFile(conventionsPath, content, 'utf-8');
|
|
50
|
-
return true;
|
|
42
|
+
if (!force) {
|
|
43
|
+
try {
|
|
44
|
+
await access(conventionsPath);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* file absent — fall through and write it */
|
|
49
|
+
}
|
|
51
50
|
}
|
|
51
|
+
const rulesDir = join(projectPath, '.claude/rules');
|
|
52
|
+
await mkdir(rulesDir, { recursive: true });
|
|
53
|
+
const content = buildConventionsContent(knowledge);
|
|
54
|
+
assertEnglishOnlyArtifactText(content, 'rule');
|
|
55
|
+
await writeFile(conventionsPath, content, 'utf-8');
|
|
56
|
+
return true;
|
|
52
57
|
}
|
|
53
58
|
//# sourceMappingURL=conventions-writer.js.map
|
|
@@ -113,6 +113,7 @@ async function configureGitignore(projectPath) {
|
|
|
113
113
|
}
|
|
114
114
|
function buildPlanuIgnoreBlock() {
|
|
115
115
|
return [
|
|
116
|
+
'planu/.runtime/',
|
|
116
117
|
'planu/*.html',
|
|
117
118
|
'planu/status.json',
|
|
118
119
|
'planu/CHANGELOG.md',
|
|
@@ -122,6 +123,14 @@ function buildPlanuIgnoreBlock() {
|
|
|
122
123
|
'planu/data/',
|
|
123
124
|
'planu/state/',
|
|
124
125
|
'planu/.locks/',
|
|
126
|
+
// SPEC-1699: legacy root files/dir folded into project.json by init_project's
|
|
127
|
+
// migration — ignored so a transitional checkout never re-commits them.
|
|
128
|
+
'planu/conventions.json',
|
|
129
|
+
'planu/context.md',
|
|
130
|
+
'planu/session-context.md',
|
|
131
|
+
'planu/session.json',
|
|
132
|
+
'planu/technology-selection.json',
|
|
133
|
+
'planu/releases/',
|
|
125
134
|
'planu/specs/**',
|
|
126
135
|
'!planu/specs/**/',
|
|
127
136
|
'!planu/specs/**/spec.md',
|
|
@@ -46,6 +46,8 @@ import { reconcileInteractiveQuestionHooks } from '../reconcile-interactive-ques
|
|
|
46
46
|
import { installSessionSafeguardHook } from '../reconcile-session-safeguard-hook.js';
|
|
47
47
|
import { resolveNewProjectOnboarding } from '../../engine/onboarding/new-project-resolver.js';
|
|
48
48
|
import { interactiveResult } from '../response-helpers.js';
|
|
49
|
+
import { scheduleHousekeepingSweeps } from './schedule-housekeeping.js';
|
|
50
|
+
import { resolveLegacyMigration } from './legacy-root-migration.js';
|
|
49
51
|
/** Frontend framework groups — mutually exclusive. Two or more detected → multi-stack conflict. */
|
|
50
52
|
const FRONTEND_FRAMEWORK_GROUPS = [
|
|
51
53
|
[
|
|
@@ -213,6 +215,11 @@ export async function handleInitProject(params, server) {
|
|
|
213
215
|
// Check if project is already initialized
|
|
214
216
|
const existing = await knowledgeStore.getKnowledge(projectId);
|
|
215
217
|
const isUpdate = hadProjectIdentity || existing !== null;
|
|
218
|
+
// SPEC-1699: fold legacy planu/ root artifacts before the sweep below runs.
|
|
219
|
+
const legacyMigration = await resolveLegacyMigration(isUpdate, projectPath);
|
|
220
|
+
// SPEC-1699: Sweep runtime residue on every init_project run, including the
|
|
221
|
+
// already-initialized early-return path below — not just the fresh-init path.
|
|
222
|
+
scheduleHousekeepingSweeps(projectPath);
|
|
216
223
|
const authorizedMigrations = params.authorizedMigrations ?? [];
|
|
217
224
|
if (isUpdate && authorizedMigrations.length === 0) {
|
|
218
225
|
const reconciliation = await reconcilePortableSpecIndex(projectPath, projectId);
|
|
@@ -234,6 +241,7 @@ export async function handleInitProject(params, server) {
|
|
|
234
241
|
failures: reconciliation.failures,
|
|
235
242
|
skippedLegacy: reconciliation.skippedLegacy,
|
|
236
243
|
strayRepoRemovals: reconciliation.strayRepoRemovals,
|
|
244
|
+
migratedLegacyPaths: legacyMigration.migrated,
|
|
237
245
|
},
|
|
238
246
|
...(reconciliation.failures.length > 0 ? { isError: true } : {}),
|
|
239
247
|
};
|
|
@@ -463,7 +471,7 @@ export async function handleInitProject(params, server) {
|
|
|
463
471
|
/* best-effort */
|
|
464
472
|
});
|
|
465
473
|
// SPEC-193: Generate portal pages (fire-and-forget)
|
|
466
|
-
void withAudit(projectPath, 'init_project', 'regeneratePages', () => regeneratePages(projectPath, ['knowledge', 'risks', 'decisions']), (pages) => ({ pages
|
|
474
|
+
void withAudit(projectPath, 'init_project', 'regeneratePages', () => regeneratePages(projectPath, ['knowledge', 'risks', 'decisions']), (pages) => ({ pages })).catch(() => {
|
|
467
475
|
/* best-effort — never fail init */
|
|
468
476
|
});
|
|
469
477
|
// SPEC-645: Detect LLM client and cache in conventions.json (fire-and-forget)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// tools/init-project/legacy-planu.ts — SPEC-1709
|
|
2
|
+
import { withAudit } from '../../engine/autopilot/audit-logger.js';
|
|
3
|
+
import { demolishLegacyPlanuRoot } from '../../engine/housekeeping/legacy-planu-demolisher.js';
|
|
4
|
+
export function scheduleLegacyPlanuDemolition(projectPath) {
|
|
5
|
+
const isTestEnvironment = Boolean(process.env.VITEST) || Boolean(process.env.PLANU_TEST_HOME);
|
|
6
|
+
if (isTestEnvironment) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
void withAudit(projectPath, 'init_project', 'demolishLegacyPlanuRoot', () => demolishLegacyPlanuRoot(), (result) => ({
|
|
10
|
+
status: result.status,
|
|
11
|
+
migrated: result.migrated.length,
|
|
12
|
+
demolishedUnmappable: result.demolishedUnmappable,
|
|
13
|
+
freedBytes: result.freedBytes,
|
|
14
|
+
})).catch(() => {
|
|
15
|
+
/* best-effort */
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=legacy-planu.js.map
|