@planu/cli 5.5.0 → 5.6.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 +73 -0
- package/dist/.planu-build.json +1 -1
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +22 -12
- 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/git/git-command-guard.d.ts +2 -0
- package/dist/engine/git/git-command-guard.js +43 -0
- package/dist/engine/git/planu-autocommit.js +2 -0
- package/dist/engine/handoff-packager.js +38 -2
- 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/runtime-residue-sweep.d.ts +9 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.js +57 -0
- 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 +1 -1
- 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/universal-rules/rules/planu-release-policy.js +1 -1
- package/dist/engine/validator/spec-compliance-runner.js +45 -0
- package/dist/hosts/claude-code/ux/mcp-resources.js +7 -23
- 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 +10 -9
- 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/git/git-helpers.js +2 -0
- 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 +10 -1
- 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/portable-index-reconciler.d.ts +1 -1
- package/dist/tools/init-project/portable-index-reconciler.js +109 -4
- package/dist/tools/init-project/runtime-residue.d.ts +2 -0
- package/dist/tools/init-project/runtime-residue.js +11 -0
- 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/housekeeping.d.ts +4 -0
- 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/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 +2 -1
- package/dist/types/spec-format.d.ts +1 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/pending-release-file.mjs +20 -4
|
@@ -1,5 +1,99 @@
|
|
|
1
|
+
import { readdir, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathExistsStrictByStat } from '../../core/shared/fs.js';
|
|
1
4
|
import { globalStore, specStore } from '../../storage/index.js';
|
|
2
5
|
import { importFilesystemSpecs } from '../../engine/spec-migrator/filesystem-import.js';
|
|
6
|
+
function isErrnoCode(error, code) {
|
|
7
|
+
return (error instanceof Error && 'code' in error && error.code === code);
|
|
8
|
+
}
|
|
9
|
+
async function countFilesRecursive(dir) {
|
|
10
|
+
let entries;
|
|
11
|
+
try {
|
|
12
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isErrnoCode(error, 'ENOENT')) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
let count = 0;
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
count += entry.isDirectory()
|
|
23
|
+
? await countFilesRecursive(join(dir, entry.name))
|
|
24
|
+
: Number(entry.isFile());
|
|
25
|
+
}
|
|
26
|
+
return count;
|
|
27
|
+
}
|
|
28
|
+
async function isPristineInitRepo(gitDir) {
|
|
29
|
+
try {
|
|
30
|
+
const [objectsCount, refsCount, hasPackedRefs, hasIndex, logsCount, hasAlternates] = await Promise.all([
|
|
31
|
+
countFilesRecursive(join(gitDir, 'objects')),
|
|
32
|
+
countFilesRecursive(join(gitDir, 'refs')),
|
|
33
|
+
pathExistsStrictByStat(join(gitDir, 'packed-refs')),
|
|
34
|
+
pathExistsStrictByStat(join(gitDir, 'index')),
|
|
35
|
+
countFilesRecursive(join(gitDir, 'logs')),
|
|
36
|
+
pathExistsStrictByStat(join(gitDir, 'objects', 'info', 'alternates')),
|
|
37
|
+
]);
|
|
38
|
+
return (objectsCount === 0 &&
|
|
39
|
+
refsCount === 0 &&
|
|
40
|
+
!hasPackedRefs &&
|
|
41
|
+
!hasIndex &&
|
|
42
|
+
logsCount === 0 &&
|
|
43
|
+
!hasAlternates);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function quarantineVerifyAndDelete(gitDir, specDirectory) {
|
|
50
|
+
const quarantinePath = join(specDirectory, `.git.stray-${String(process.pid)}`);
|
|
51
|
+
try {
|
|
52
|
+
await rename(gitDir, quarantinePath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (await isPristineInitRepo(quarantinePath)) {
|
|
58
|
+
await rm(quarantinePath, { recursive: true, force: true });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
await rename(quarantinePath, gitDir).catch(() => undefined);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
async function healStraySpecRepos(projectPath) {
|
|
65
|
+
const specsRoot = join(projectPath, 'planu', 'specs');
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await readdir(specsRoot, { withFileTypes: true });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { removals: [], failures: [] };
|
|
72
|
+
}
|
|
73
|
+
const removals = [];
|
|
74
|
+
const failures = [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory()) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const specDirectory = join(specsRoot, entry.name);
|
|
80
|
+
const gitDir = join(specDirectory, '.git');
|
|
81
|
+
const gitDirStat = await stat(gitDir).catch(() => undefined);
|
|
82
|
+
if (!gitDirStat?.isDirectory()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const relativePath = join('planu', 'specs', entry.name, '.git');
|
|
86
|
+
const isCandidate = await isPristineInitRepo(gitDir);
|
|
87
|
+
const removed = isCandidate && (await quarantineVerifyAndDelete(gitDir, specDirectory));
|
|
88
|
+
if (removed) {
|
|
89
|
+
removals.push(relativePath);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
failures.push({ specId: entry.name, path: relativePath, code: 'STRAY_REPO_NOT_PRISTINE' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { removals, failures };
|
|
96
|
+
}
|
|
3
97
|
function classifyFailures(failures, indexedSpecIds) {
|
|
4
98
|
const remainingFailures = [];
|
|
5
99
|
const skippedLegacy = [];
|
|
@@ -24,6 +118,7 @@ const NEXT_ACTION_BY_CODE = {
|
|
|
24
118
|
INVALID_CONTRACT: 'add the required portable frontmatter fields (id, title, type, scope, status, risk, target, difficulty)',
|
|
25
119
|
CREATE_FAILED: 'retry init_project once the underlying store write succeeds',
|
|
26
120
|
INCOMPATIBLE_IDENTITY: 'reconcile the spec identity (id, title, slug, uuid) with the indexed record',
|
|
121
|
+
STRAY_REPO_NOT_PRISTINE: 'inspect the embedded .git manually — it holds refs, staged state, or history and was left untouched',
|
|
27
122
|
};
|
|
28
123
|
/** Autopilot-first summary of fatal reconciliation failures: what failed, why, and the next action. */
|
|
29
124
|
export function describeReconciliationFailures(failures) {
|
|
@@ -43,19 +138,22 @@ export function describeReconciliationFailures(failures) {
|
|
|
43
138
|
return `Portable spec index reconciliation failed for ${String(failures.length)} contract(s) (${codeSummary}). Affected: ${topPaths}${remainder}. Next: ${nextActions}.`;
|
|
44
139
|
}
|
|
45
140
|
/** Autopilot-first summary of a successful reconciliation, including any legacy skips. */
|
|
46
|
-
export function describeReconciliationSuccess(repositoryFilesChanged, skippedLegacy) {
|
|
141
|
+
export function describeReconciliationSuccess(repositoryFilesChanged, skippedLegacy, strayRepoRemovals = []) {
|
|
47
142
|
const base = repositoryFilesChanged.length === 0
|
|
48
143
|
? 'Project already initialized. Safe update reconciled the portable spec index without changing repository files.'
|
|
49
144
|
: `Project already initialized. Safe update reconciled the portable spec index and refreshed ${String(repositoryFilesChanged.length)} core host asset file(s).`;
|
|
50
145
|
const skippedSuffix = skippedLegacy.length === 0
|
|
51
146
|
? ''
|
|
52
147
|
: ` Skipped ${String(skippedLegacy.length)} already-indexed legacy contract(s).`;
|
|
53
|
-
|
|
148
|
+
const strayRepoSuffix = strayRepoRemovals.length === 0
|
|
149
|
+
? ''
|
|
150
|
+
: ` Removed ${String(strayRepoRemovals.length)} stray embedded git repositor${strayRepoRemovals.length === 1 ? 'y' : 'ies'} from spec folders.`;
|
|
151
|
+
return base + skippedSuffix + strayRepoSuffix;
|
|
54
152
|
}
|
|
55
153
|
/** Autopilot-first message for a reconciliation outcome, success or fatal-failure. */
|
|
56
154
|
export function describeReconciliationOutcome(reconciliation, repositoryFilesChanged) {
|
|
57
155
|
return reconciliation.failures.length === 0
|
|
58
|
-
? describeReconciliationSuccess(repositoryFilesChanged, reconciliation.skippedLegacy)
|
|
156
|
+
? describeReconciliationSuccess(repositoryFilesChanged, reconciliation.skippedLegacy, reconciliation.strayRepoRemovals)
|
|
59
157
|
: describeReconciliationFailures(reconciliation.failures);
|
|
60
158
|
}
|
|
61
159
|
/** Rebuild the mutable external index from the repository-owned portable contracts. */
|
|
@@ -70,6 +168,13 @@ export async function reconcilePortableSpecIndex(projectPath, projectId, deps =
|
|
|
70
168
|
});
|
|
71
169
|
const indexedSpecIds = new Set((await deps.listSpecs(projectId)).map((spec) => spec.id));
|
|
72
170
|
const { failures, skippedLegacy } = classifyFailures(result.failures, indexedSpecIds);
|
|
73
|
-
|
|
171
|
+
const { removals: strayRepoRemovals, failures: strayRepoFailures } = await healStraySpecRepos(projectPath);
|
|
172
|
+
return {
|
|
173
|
+
...result,
|
|
174
|
+
failures: [...failures, ...strayRepoFailures],
|
|
175
|
+
skippedLegacy,
|
|
176
|
+
repositoryFilesChanged: [],
|
|
177
|
+
strayRepoRemovals,
|
|
178
|
+
};
|
|
74
179
|
}
|
|
75
180
|
//# sourceMappingURL=portable-index-reconciler.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
|
|
@@ -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,
|
|
@@ -146,4 +146,8 @@ export interface HousekeepingHistoryEntry {
|
|
|
146
146
|
prevSha: string;
|
|
147
147
|
sha256: string;
|
|
148
148
|
}
|
|
149
|
+
export interface RuntimeResidueSweepResult {
|
|
150
|
+
/** Absolute paths of every legacy runtime artifact removed from planu/. */
|
|
151
|
+
removed: string[];
|
|
152
|
+
}
|
|
149
153
|
//# 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
|
|
@@ -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
|
package/dist/types/spec/index.js
CHANGED
|
@@ -215,7 +215,7 @@ export interface SpecMigrationDeps {
|
|
|
215
215
|
listSpecs: (projectId: string) => Promise<Spec[]>;
|
|
216
216
|
updateSpec: (projectId: string, specId: string, updates: Partial<Spec>) => Promise<Spec>;
|
|
217
217
|
}
|
|
218
|
-
export type FilesystemImportFailureCode = 'READ_FAILED' | 'INVALID_CONTRACT' | 'CREATE_FAILED' | 'INCOMPATIBLE_IDENTITY';
|
|
218
|
+
export type FilesystemImportFailureCode = 'READ_FAILED' | 'INVALID_CONTRACT' | 'CREATE_FAILED' | 'INCOMPATIBLE_IDENTITY' | 'STRAY_REPO_NOT_PRISTINE';
|
|
219
219
|
export interface FilesystemImportFailure {
|
|
220
220
|
specId: string;
|
|
221
221
|
path: string;
|
|
@@ -242,6 +242,7 @@ export interface SkippedLegacyContract {
|
|
|
242
242
|
export interface PortableIndexReconciliationResult extends FilesystemImportResult {
|
|
243
243
|
repositoryFilesChanged: string[];
|
|
244
244
|
skippedLegacy: SkippedLegacyContract[];
|
|
245
|
+
strayRepoRemovals: string[];
|
|
245
246
|
}
|
|
246
247
|
export interface ImportEntryContext {
|
|
247
248
|
projectPath: string;
|
|
@@ -72,6 +72,7 @@ export interface PlanuCanonicalPathPolicy {
|
|
|
72
72
|
readonly canonicalSpecFiles: readonly string[];
|
|
73
73
|
readonly canonicalSpecDirs: readonly string[];
|
|
74
74
|
readonly forbiddenHostAssetRootDirs: readonly string[];
|
|
75
|
+
readonly runtimeHomeDir: string;
|
|
75
76
|
readonly generatedRuntimePatterns: readonly string[];
|
|
76
77
|
readonly legacyMergeBeforeDeleteFiles: readonly string[];
|
|
77
78
|
}
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.6.0",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|
|
@@ -116,15 +116,31 @@ export function isCommitReachable(repoRoot, commit, ontoCommit) {
|
|
|
116
116
|
throw new Error(`git merge-base --is-ancestor exited with status ${String(result.status)}`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
const temporaryPath = `${pendingPath}.${process.pid}.${Date.now()}.tmp`;
|
|
119
|
+
async function readJsonObject(path) {
|
|
121
120
|
try {
|
|
122
|
-
|
|
121
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
122
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code === 'ENOENT') return {};
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* SPEC-1699: writes `entries` into the `releases` field of the committed
|
|
131
|
+
* planu/project.json, preserving every other field already on disk.
|
|
132
|
+
*/
|
|
133
|
+
export async function writePendingReleaseLedger(projectJsonPath, entries) {
|
|
134
|
+
const project = await readJsonObject(projectJsonPath);
|
|
135
|
+
const nextProject = { ...project, releases: entries };
|
|
136
|
+
const temporaryPath = `${projectJsonPath}.${process.pid}.${Date.now()}.tmp`;
|
|
137
|
+
try {
|
|
138
|
+
await writeFile(temporaryPath, `${JSON.stringify(nextProject, null, 2)}\n`, {
|
|
123
139
|
encoding: 'utf8',
|
|
124
140
|
flag: 'wx',
|
|
125
141
|
mode: 0o644,
|
|
126
142
|
});
|
|
127
|
-
await rename(temporaryPath,
|
|
143
|
+
await rename(temporaryPath, projectJsonPath);
|
|
128
144
|
} catch (error) {
|
|
129
145
|
await rm(temporaryPath, { force: true });
|
|
130
146
|
throw error;
|