@planu/cli 5.3.52 → 5.3.55
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 +80 -0
- package/dist/cli/commands/package-handoff.d.ts +3 -0
- package/dist/cli/commands/package-handoff.js +43 -0
- package/dist/cli/commands/spec.js +3 -8
- package/dist/cli/commands/status.d.ts +52 -1
- package/dist/cli/commands/status.js +39 -35
- package/dist/cli/router.js +5 -2
- package/dist/engine/git/canonical-branch.d.ts +2 -0
- package/dist/engine/git/canonical-branch.js +31 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
- package/dist/engine/handoff-artifacts/schemas.js +1 -0
- package/dist/engine/human-summary.js +1 -1
- package/dist/engine/lifecycle-reconciliation.js +3 -0
- package/dist/engine/scope-boundaries/contradiction-checker.js +29 -5
- package/dist/engine/sdd-model-routing.js +7 -2
- package/dist/engine/spec-format/lean-spec-generator.js +1 -1
- package/dist/engine/staleness/stale-implementing.js +19 -58
- package/dist/engine/text-signal-boundaries.js +3 -1
- package/dist/engine/validator/spec-compliance-runner.d.ts +1 -0
- package/dist/engine/validator/spec-compliance-runner.js +13 -6
- package/dist/engine/validator/validation-report-writer.js +1 -0
- package/dist/engine/workflow-validator/worktree-protocol.js +45 -36
- package/dist/storage/transition-log.d.ts +2 -14
- package/dist/storage/transition-log.js +84 -45
- package/dist/tools/challenge-spec/challenge-report.js +42 -11
- package/dist/tools/challenge-spec/scenarios-utils.js +9 -2
- package/dist/tools/generate-orchestration-script.js +2 -1
- package/dist/tools/register-platform-tools/design-stack-tools.js +1 -1
- package/dist/tools/suggest-tooling/orchestration-generator.js +2 -2
- package/dist/tools/sync-spec-state-handler.js +20 -7
- package/dist/tools/update-status/dod-gates.js +36 -6
- package/dist/tools/update-status/file-sync.d.ts +1 -0
- package/dist/tools/update-status/file-sync.js +45 -13
- package/dist/tools/update-status/index.js +12 -1
- package/dist/tools/update-status/transition-guard.d.ts +1 -0
- package/dist/tools/update-status/transition-guard.js +50 -4
- package/dist/tools/validate.js +5 -2
- package/dist/types/handoff-artifacts.d.ts +2 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -17,7 +17,7 @@ import { formatKeyValue } from '../output-formatter.js';
|
|
|
17
17
|
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
18
18
|
import { verifyCurrentDoneReceipt } from './done-receipt-verifier.js';
|
|
19
19
|
import { projectDataDir } from '../../storage/base-store.js';
|
|
20
|
-
import { readTransitionLog } from '../../storage/transition-log.js';
|
|
20
|
+
import { readTransitionLog, transitionLogPath } from '../../storage/transition-log.js';
|
|
21
21
|
import { ImplementationReviewV1Schema, ReviewFeedbackV1Schema, ValidationReportV1Schema, } from '../../engine/handoff-artifacts/schemas.js';
|
|
22
22
|
import { renderSchemaSkeleton } from '../../engine/handoff-artifacts/schema-skeleton.js';
|
|
23
23
|
export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
|
|
@@ -30,6 +30,11 @@ function lazyTemplate(render) {
|
|
|
30
30
|
return cached;
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
function schemaBlockers(errors) {
|
|
34
|
+
return errors
|
|
35
|
+
.filter((issue) => issue.code !== 'ARTIFACT_NOT_FOUND' && issue.code !== 'INVALID_JSON')
|
|
36
|
+
.map((issue) => `${issue.path}: ${issue.message}`);
|
|
37
|
+
}
|
|
33
38
|
export const reviewFeedbackTemplate = lazyTemplate(() => renderSchemaSkeleton(ReviewFeedbackV1Schema, {
|
|
34
39
|
overrides: {
|
|
35
40
|
schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
|
|
@@ -525,6 +530,7 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
|
|
|
525
530
|
error: 'validation_report_invalid',
|
|
526
531
|
message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
|
|
527
532
|
gates: [],
|
|
533
|
+
blockers: schemaBlockers(result.errors),
|
|
528
534
|
fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
|
|
529
535
|
template: validationReportTemplate(),
|
|
530
536
|
}),
|
|
@@ -602,7 +608,7 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
|
602
608
|
message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
603
609
|
? 'No spec review feedback exists for this spec. An independent planu-spec-reviewer must review the spec and write its evidence before approval.'
|
|
604
610
|
: 'Spec review feedback is malformed or uses an obsolete schema.',
|
|
605
|
-
blockers:
|
|
611
|
+
blockers: schemaBlockers(result.errors),
|
|
606
612
|
fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
607
613
|
? 'Have planu-spec-reviewer write review_feedback.md (ReviewFeedbackV1) into the handoff store, then retry update_status(approved).'
|
|
608
614
|
: 'Have planu-spec-reviewer rewrite review_feedback.md as valid ReviewFeedbackV1 evidence, then retry approval.',
|
|
@@ -666,7 +672,7 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
|
666
672
|
message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
667
673
|
? 'No implementation review exists for this spec. An independent planu-implementation-reviewer must review the implementation and write its own evidence before done.'
|
|
668
674
|
: 'Implementation review evidence is malformed or uses an obsolete schema.',
|
|
669
|
-
blockers:
|
|
675
|
+
blockers: schemaBlockers(result.errors),
|
|
670
676
|
fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
671
677
|
? 'Have planu-implementation-reviewer write implementation_review.json (ImplementationReviewV1) into the handoff store, then retry update_status(done).'
|
|
672
678
|
: 'Have planu-implementation-reviewer rewrite implementation_review.json as valid ImplementationReviewV1 evidence, then retry done.',
|
|
@@ -704,7 +710,22 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
|
704
710
|
fixHint: 'Have planu-implementation-reviewer (with the recognized identity) write the review artifact, then retry done.',
|
|
705
711
|
});
|
|
706
712
|
}
|
|
707
|
-
|
|
713
|
+
let implementerActor;
|
|
714
|
+
try {
|
|
715
|
+
implementerActor = await findImplementingActor(specId, projectId);
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
/* reliability-optional: TRANSITION_LOG_GATE_READ — typed gate error blocks done */
|
|
719
|
+
reportClassifiedDegradation('TRANSITION_LOG_GATE_READ', err);
|
|
720
|
+
return implementationReviewGateError({
|
|
721
|
+
specId,
|
|
722
|
+
error: 'implementing_actor_unreadable',
|
|
723
|
+
message: `Could not read the transition log to find the implementing actor: ${err instanceof Error ? err.message : String(err)}`,
|
|
724
|
+
blockers: [],
|
|
725
|
+
fixHint: 'Fix the transition log so it can be read, then retry update_status(done).',
|
|
726
|
+
artifactPath: transitionLogPath(projectId),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
708
729
|
if (implementerActor !== undefined && implementerActor === review.reviewer.agent) {
|
|
709
730
|
return implementationReviewGateError({
|
|
710
731
|
specId,
|
|
@@ -739,7 +760,8 @@ async function findImplementingActor(specId, projectId) {
|
|
|
739
760
|
return undefined;
|
|
740
761
|
}
|
|
741
762
|
function implementationReviewGateError(args) {
|
|
742
|
-
const artifactPath =
|
|
763
|
+
const artifactPath = args.artifactPath ??
|
|
764
|
+
`external Planu project data: handoffs/${args.specId}/implementation_review.json`;
|
|
743
765
|
return {
|
|
744
766
|
content: [
|
|
745
767
|
{
|
|
@@ -766,6 +788,7 @@ function implementationReviewGateError(args) {
|
|
|
766
788
|
}
|
|
767
789
|
function validationReportGateError(args) {
|
|
768
790
|
const artifactPath = `external Planu project data: handoffs/${args.specId}/validation-report.json`;
|
|
791
|
+
const blockerCount = args.blockers && args.blockers.length > 0 ? args.blockers.length : undefined;
|
|
769
792
|
return {
|
|
770
793
|
content: [
|
|
771
794
|
{
|
|
@@ -776,6 +799,8 @@ function validationReportGateError(args) {
|
|
|
776
799
|
artifactPath,
|
|
777
800
|
failedGates: args.gates.filter((gate) => !gate.passed).length,
|
|
778
801
|
totalGates: args.gates.length,
|
|
802
|
+
blockers: blockerCount,
|
|
803
|
+
firstBlocker: args.blockers?.[0],
|
|
779
804
|
fixHint: args.fixHint,
|
|
780
805
|
}, 'Validation report gate failed'),
|
|
781
806
|
},
|
|
@@ -784,7 +809,12 @@ function validationReportGateError(args) {
|
|
|
784
809
|
structuredContent: {
|
|
785
810
|
error: args.error,
|
|
786
811
|
code: 422,
|
|
787
|
-
context: {
|
|
812
|
+
context: {
|
|
813
|
+
specId: args.specId,
|
|
814
|
+
artifactPath,
|
|
815
|
+
gates: args.gates,
|
|
816
|
+
...(args.blockers ? { blockers: args.blockers } : {}),
|
|
817
|
+
},
|
|
788
818
|
fixHint: args.fixHint,
|
|
789
819
|
...(args.template ? { template: args.template } : {}),
|
|
790
820
|
},
|
|
@@ -12,6 +12,7 @@ export interface FrontmatterSyncWarning {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function syncSpecFiles(updatedSpec: Spec, _currentStatus: SpecStatus, newStatus: SpecStatus, projectPath?: string, qualityWarnings?: readonly string[]): Promise<{
|
|
14
14
|
warning?: FrontmatterSyncWarning;
|
|
15
|
+
title?: string;
|
|
15
16
|
}>;
|
|
16
17
|
export declare function tryReconcile(newStatus: string, specId: string, projectId: string): Promise<string | null>;
|
|
17
18
|
/**
|
|
@@ -75,6 +75,24 @@ async function autoCaptureLessonEstimation(spec, actuals, accuracy, projectId, p
|
|
|
75
75
|
}
|
|
76
76
|
const SPEC_BODY_SHRINK_ABSOLUTE_THRESHOLD = 200;
|
|
77
77
|
const SPEC_BODY_SHRINK_RATIO_THRESHOLD = 0.05;
|
|
78
|
+
const SPEC_ARTIFACT_ABSENT_CODE = 'SPEC_ARTIFACT_ABSENT';
|
|
79
|
+
function specArtifactAbsentError() {
|
|
80
|
+
const error = new Error('Spec artifact is absent');
|
|
81
|
+
error.code = SPEC_ARTIFACT_ABSENT_CODE;
|
|
82
|
+
return error;
|
|
83
|
+
}
|
|
84
|
+
function isAbsentSpecArtifactFailure(err, resolvedArtifactPath) {
|
|
85
|
+
if (!(err instanceof Error)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const code = err.code;
|
|
89
|
+
if (code === SPEC_ARTIFACT_ABSENT_CODE) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return (code === 'ENOENT' &&
|
|
93
|
+
resolvedArtifactPath !== undefined &&
|
|
94
|
+
err.path === resolvedArtifactPath);
|
|
95
|
+
}
|
|
78
96
|
async function resolveSpecPathForTransition(spec, projectPath) {
|
|
79
97
|
if (!projectPath) {
|
|
80
98
|
return spec.specPath;
|
|
@@ -100,11 +118,16 @@ async function resolveSpecPathForTransition(spec, projectPath) {
|
|
|
100
118
|
isAbsolute(relativeCandidate)) {
|
|
101
119
|
throw new Error('SPEC_PATH_RELOCALIZATION_FAILED: specPath escapes the selected root');
|
|
102
120
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
121
|
+
const rootRealpath = await realpath(selectedRoot);
|
|
122
|
+
const [candidateRealpath, candidateStat] = await Promise.all([
|
|
105
123
|
realpath(candidate),
|
|
106
124
|
lstat(candidate),
|
|
107
|
-
])
|
|
125
|
+
]).catch((err) => {
|
|
126
|
+
if (err instanceof Error && err.code === 'ENOENT') {
|
|
127
|
+
throw specArtifactAbsentError();
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
});
|
|
108
131
|
const realRelative = relative(rootRealpath, candidateRealpath);
|
|
109
132
|
if (candidateStat.isSymbolicLink() ||
|
|
110
133
|
!candidateStat.isFile() ||
|
|
@@ -138,15 +161,20 @@ function assertSpecBodyIntegrity(args) {
|
|
|
138
161
|
}
|
|
139
162
|
export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, projectPath, qualityWarnings = []) {
|
|
140
163
|
let warning;
|
|
164
|
+
let refreshedTitle;
|
|
165
|
+
let transitionSpecPath;
|
|
141
166
|
if (updatedSpec.specPath) {
|
|
142
167
|
try {
|
|
143
|
-
|
|
168
|
+
transitionSpecPath = await resolveSpecPathForTransition(updatedSpec, projectPath);
|
|
144
169
|
const specContent = await readFile(transitionSpecPath, 'utf-8');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
const parsedMetadata = parseFrontmatter(specContent).metadata;
|
|
171
|
+
if (projectPath && parsedMetadata.id !== updatedSpec.id) {
|
|
172
|
+
throw new Error('SPEC_PATH_RELOCALIZATION_FAILED: spec frontmatter ID does not match');
|
|
173
|
+
}
|
|
174
|
+
if (typeof parsedMetadata.title === 'string' &&
|
|
175
|
+
parsedMetadata.title.trim() !== '' &&
|
|
176
|
+
parsedMetadata.title !== updatedSpec.title) {
|
|
177
|
+
refreshedTitle = parsedMetadata.title;
|
|
150
178
|
}
|
|
151
179
|
const { updateFrontmatterField } = await import('../../engine/frontmatter-parser.js');
|
|
152
180
|
let updatedContent = updateFrontmatterField(specContent, 'status', newStatus);
|
|
@@ -191,19 +219,23 @@ export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, proj
|
|
|
191
219
|
}
|
|
192
220
|
}
|
|
193
221
|
catch (err) {
|
|
194
|
-
|
|
222
|
+
if (newStatus === 'discarded' && isAbsentSpecArtifactFailure(err, transitionSpecPath)) {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
195
225
|
const msg = redactFailureMessage(err);
|
|
196
|
-
// SPEC-698: log AND surface the warning so the user sees it in the tool response
|
|
197
226
|
reportClassifiedDegradation('SPEC_FRONTMATTER_SYNC', err);
|
|
198
227
|
warning = { specId: updatedSpec.id, specPath: updatedSpec.specPath, reason: msg };
|
|
199
228
|
}
|
|
200
229
|
}
|
|
201
|
-
else {
|
|
230
|
+
else if (newStatus !== 'discarded') {
|
|
202
231
|
const reason = 'specPath not set on spec — spec.md not updated on disk. Run reconcile_spec to re-link this spec to its file.';
|
|
203
232
|
console.warn(`[Planu] update_status: ${reason} (${updatedSpec.id})`);
|
|
204
233
|
warning = { specId: updatedSpec.id, specPath: null, reason };
|
|
205
234
|
}
|
|
206
|
-
|
|
235
|
+
if (warning) {
|
|
236
|
+
return { warning };
|
|
237
|
+
}
|
|
238
|
+
return refreshedTitle ? { title: refreshedTitle } : {};
|
|
207
239
|
}
|
|
208
240
|
/** Flip all ` done: false` lines to ` done: true` in spec content.
|
|
209
241
|
* Safe: in lean YAML spec, `done: false` only appears inside criteria items. */
|
|
@@ -9,7 +9,7 @@ import { cascadeCheck } from '../../engine/spec-versioner.js';
|
|
|
9
9
|
import { runComplianceGates } from '../update-status-convention-gate.js';
|
|
10
10
|
import { runDoneActions, runDoneSideEffects, runImplementingActions, runImplementingSideEffects, } from '../update-status-actions.js';
|
|
11
11
|
import { compactObj } from '../../engine/compact-obj.js';
|
|
12
|
-
import { checkTransition, checkDorGate, checkAmbiguityGate, checkReadinessGate, checkChallengeGate, checkImplementationDependencyGate, resolveAutoAdvanceSteps, isReverseTransition, validateReverseTransition, } from './transition-guard.js';
|
|
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';
|
|
14
14
|
import { checkApprovalGate } from '../../engine/approval-workflow.js';
|
|
15
15
|
import * as approvalStore from '../../storage/approval-store.js';
|
|
@@ -667,6 +667,10 @@ async function commitTransitionWithNarrowLock(args) {
|
|
|
667
667
|
const updatedSpec = transitionRecord.spec;
|
|
668
668
|
const syncResult = await syncSpecFiles(updatedSpec, args.originalStatus, args.newStatus, args.projectPath, args.qualityWarnings);
|
|
669
669
|
if (!syncResult.warning) {
|
|
670
|
+
if (syncResult.title) {
|
|
671
|
+
updatedSpec.title = syncResult.title;
|
|
672
|
+
await specStore.updateSpec(args.projectId, args.specId, { title: syncResult.title });
|
|
673
|
+
}
|
|
670
674
|
return { ok: true, transitionRecord, updatedSpec };
|
|
671
675
|
}
|
|
672
676
|
try {
|
|
@@ -959,6 +963,10 @@ export async function handleUpdateStatus(params, server) {
|
|
|
959
963
|
if (dependencyGate) {
|
|
960
964
|
return dependencyGate;
|
|
961
965
|
}
|
|
966
|
+
const specArtifactGate = await checkSpecArtifactCommittedGate(spec, 'implementing', transitionProjectPath);
|
|
967
|
+
if (specArtifactGate) {
|
|
968
|
+
return specArtifactGate;
|
|
969
|
+
}
|
|
962
970
|
}
|
|
963
971
|
// SPEC-964: Challenge gate — block 'review' if challenge_spec was never run
|
|
964
972
|
const challengeGateStatus = plannedStatuses.includes('review') ? 'review' : newStatus;
|
|
@@ -1344,6 +1352,9 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1344
1352
|
return persistence.error;
|
|
1345
1353
|
}
|
|
1346
1354
|
const { transitionRecord, updatedSpec } = persistence;
|
|
1355
|
+
if (updatedSpec.title && updatedSpec.title !== spec.title) {
|
|
1356
|
+
spec.title = updatedSpec.title;
|
|
1357
|
+
}
|
|
1347
1358
|
const frontmatterSyncWarnings = [];
|
|
1348
1359
|
const transitionId = transitionRecord.transitionId;
|
|
1349
1360
|
const committedAt = transitionRecord.timestamp;
|
|
@@ -60,6 +60,7 @@ export declare function checkImplementationDependencyGate(spec: Spec, allSpecs:
|
|
|
60
60
|
* Fails closed when the spec cannot be scored.
|
|
61
61
|
*/
|
|
62
62
|
export declare function checkAmbiguityGate(spec: Spec, newStatus: SpecStatus): Promise<ToolResult | null>;
|
|
63
|
+
export declare function checkSpecArtifactCommittedGate(spec: Spec, newStatus: SpecStatus, projectPath: string | undefined): Promise<ToolResult | null>;
|
|
63
64
|
/**
|
|
64
65
|
* SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
|
|
65
66
|
* quality is too low or the persisted spec document is not English.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// tools/update-status/transition-guard.ts — Valid state transitions and DoR gate
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
4
|
import { ti } from '../../i18n/index.js';
|
|
4
5
|
import { validateDoR } from '../../engine/dor-dod.js';
|
|
5
6
|
import { dispatchFeedbackEvent } from '../learn.js';
|
|
@@ -232,6 +233,47 @@ export async function checkAmbiguityGate(spec, newStatus) {
|
|
|
232
233
|
},
|
|
233
234
|
};
|
|
234
235
|
}
|
|
236
|
+
export async function checkSpecArtifactCommittedGate(spec, newStatus, projectPath) {
|
|
237
|
+
if (newStatus !== 'implementing' || !projectPath || !spec.specPath) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const absoluteSpecPath = isAbsolute(spec.specPath)
|
|
241
|
+
? spec.specPath
|
|
242
|
+
: join(projectPath, spec.specPath);
|
|
243
|
+
try {
|
|
244
|
+
const { git } = await import('../git/git-helpers.js');
|
|
245
|
+
const { stdout } = await git(projectPath, ['status', '--porcelain', '--', absoluteSpecPath]);
|
|
246
|
+
if (stdout.trim().length === 0) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
reportClassifiedDegradation('SPEC_ARTIFACT_GIT_STATUS_UNAVAILABLE', error);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const fixHint = `Commit ${absoluteSpecPath} (git add "${absoluteSpecPath}" && git commit) then retry update_status(specId: "${spec.id}", status: "implementing").`;
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: formatKeyValue({
|
|
260
|
+
error: 'SPEC_ARTIFACT_UNCOMMITTED',
|
|
261
|
+
message: `spec.md for ${spec.id} has uncommitted changes. The approved contract on disk must match what git holds before an implementer is dispatched.`,
|
|
262
|
+
specId: spec.id,
|
|
263
|
+
specPath: absoluteSpecPath,
|
|
264
|
+
fixHint,
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
isError: true,
|
|
269
|
+
structuredContent: {
|
|
270
|
+
error: 'SPEC_ARTIFACT_UNCOMMITTED',
|
|
271
|
+
specId: spec.id,
|
|
272
|
+
specPath: absoluteSpecPath,
|
|
273
|
+
fixHint,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
235
277
|
/**
|
|
236
278
|
* SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
|
|
237
279
|
* quality is too low or the persisted spec document is not English.
|
|
@@ -511,6 +553,7 @@ function gateUnavailableResult(code, message) {
|
|
|
511
553
|
function readinessUnavailable(code, message, qualityWarnings = []) {
|
|
512
554
|
return { blockResult: gateUnavailableResult(code, message), qualityWarnings };
|
|
513
555
|
}
|
|
556
|
+
const CHALLENGE_RESOLUTION_INSTRUCTIONS = 'Add a "### Challenge Resolution" subsection to the spec\'s spec.md with one "- " bullet per unresolved finding, each bullet containing that finding\'s exact scenario text plus your mitigation, then re-run challenge_spec to re-derive the report.';
|
|
514
557
|
/**
|
|
515
558
|
* SPEC-964: Challenge gate — block 'review' if challenge_spec was never run
|
|
516
559
|
* or does not contain enough explicit resolution evidence.
|
|
@@ -584,7 +627,7 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
584
627
|
content: [
|
|
585
628
|
{
|
|
586
629
|
type: 'text',
|
|
587
|
-
text: `Challenge gate blocked: ${String(unresolvedCriticalCount)} unresolved critical finding(s) remain (addressed ${String(addressedCount)}/${String(requiredCount)} required). Record
|
|
630
|
+
text: `Challenge gate blocked: ${String(unresolvedCriticalCount)} unresolved critical finding(s) remain (addressed ${String(addressedCount)}/${String(requiredCount)} required). Record resolution evidence for every critical finding before retrying.`,
|
|
588
631
|
},
|
|
589
632
|
],
|
|
590
633
|
isError: true,
|
|
@@ -595,7 +638,8 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
595
638
|
requiredCount,
|
|
596
639
|
unresolvedCount,
|
|
597
640
|
unresolvedCriticalCount,
|
|
598
|
-
|
|
641
|
+
unresolvedCriticalIdentities: gateState.unresolvedCriticalIdentities,
|
|
642
|
+
fixHint: `${CHALLENGE_RESOLUTION_INSTRUCTIONS} Then retry update_status(specId="${spec.id}", status="review").`,
|
|
599
643
|
},
|
|
600
644
|
};
|
|
601
645
|
}
|
|
@@ -604,7 +648,7 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
604
648
|
content: [
|
|
605
649
|
{
|
|
606
650
|
type: 'text',
|
|
607
|
-
text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(requiredCount)} required). Record
|
|
651
|
+
text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(requiredCount)} required). Record resolution evidence for discovered findings before retrying.`,
|
|
608
652
|
},
|
|
609
653
|
],
|
|
610
654
|
isError: true,
|
|
@@ -613,7 +657,9 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
613
657
|
code: 'INSUFFICIENT_CHALLENGES',
|
|
614
658
|
addressedCount,
|
|
615
659
|
requiredCount,
|
|
616
|
-
|
|
660
|
+
unresolvedCount,
|
|
661
|
+
unresolvedIdentities: gateState.unresolvedIdentities,
|
|
662
|
+
fixHint: `${CHALLENGE_RESOLUTION_INSTRUCTIONS} Then retry update_status(specId="${spec.id}", status="review").`,
|
|
617
663
|
},
|
|
618
664
|
};
|
|
619
665
|
}
|
package/dist/tools/validate.js
CHANGED
|
@@ -688,8 +688,11 @@ function buildExecutableCoverage(specCompliance) {
|
|
|
688
688
|
const matches = specCompliance.scenarios
|
|
689
689
|
.filter((scenario) => scenario.verdict === 'pass')
|
|
690
690
|
.map((scenario) => scenario.title);
|
|
691
|
+
const indeterminate = specCompliance.scenarios
|
|
692
|
+
.filter((scenario) => scenario.verdict !== 'pass' && scenario.unverifiable)
|
|
693
|
+
.map((scenario) => scenario.title);
|
|
691
694
|
const missing = specCompliance.scenarios
|
|
692
|
-
.filter((scenario) => scenario.verdict !== 'pass')
|
|
695
|
+
.filter((scenario) => scenario.verdict !== 'pass' && !scenario.unverifiable)
|
|
693
696
|
.map((scenario) => scenario.title);
|
|
694
697
|
return {
|
|
695
698
|
score: specCompliance.score,
|
|
@@ -697,7 +700,7 @@ function buildExecutableCoverage(specCompliance) {
|
|
|
697
700
|
fieldsTotal: specCompliance.scenarios.length,
|
|
698
701
|
matches,
|
|
699
702
|
missing,
|
|
700
|
-
indeterminate
|
|
703
|
+
indeterminate,
|
|
701
704
|
};
|
|
702
705
|
}
|
|
703
706
|
function buildEffectiveValidationResult(result, specCompliance) {
|
|
@@ -86,6 +86,7 @@ export interface ValidationReportV1 {
|
|
|
86
86
|
title: string;
|
|
87
87
|
verdict: 'pass' | 'fail' | 'missing';
|
|
88
88
|
evidence: string[];
|
|
89
|
+
unverifiable?: true;
|
|
89
90
|
}[];
|
|
90
91
|
};
|
|
91
92
|
minimalityReport?: MinimalImplementationReport;
|
|
@@ -133,6 +134,7 @@ export type ReconciliationSpecSync = (spec: Spec, currentStatus: 'implementing'
|
|
|
133
134
|
warning?: {
|
|
134
135
|
reason: string;
|
|
135
136
|
};
|
|
137
|
+
title?: string;
|
|
136
138
|
}>;
|
|
137
139
|
export interface ArtifactPayloadMap {
|
|
138
140
|
intake: IntakeV1;
|
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.3.
|
|
5
|
+
"version": "5.3.55",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|