@planu/cli 4.10.5 → 4.10.7
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 +20 -0
- package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
- package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
- package/dist/engine/evidence-index/done-drift.js +4 -1
- package/dist/engine/local-first/tool-classification.d.ts +7 -0
- package/dist/engine/local-first/tool-classification.js +313 -0
- package/dist/engine/qa-gate.js +13 -1
- package/dist/engine/scope-boundaries/scope-validator.js +14 -2
- package/dist/engine/validator/validation-report-writer.js +3 -4
- package/dist/tools/audit.js +1 -1
- package/dist/tools/challenge-spec.js +52 -2
- package/dist/tools/check-readiness.js +15 -8
- package/dist/tools/data-governance/audit-handler.js +21 -6
- package/dist/tools/data-governance/detect-handler.js +32 -15
- package/dist/tools/define-ui-contract.js +1 -4
- package/dist/tools/design-schema.js +1 -4
- package/dist/tools/detect-drift.js +48 -28
- package/dist/tools/flag-spec-gap.js +47 -12
- package/dist/tools/generate-sub-agent.js +2 -4
- package/dist/tools/list-specs.js +15 -59
- package/dist/tools/orchestrate-locking.js +15 -4
- package/dist/tools/output-compressor.d.ts +14 -1
- package/dist/tools/output-compressor.js +121 -2
- package/dist/tools/package-handoff.js +136 -12
- package/dist/tools/reverse-engineer/handler.js +9 -10
- package/dist/tools/rollback-release.js +20 -2
- package/dist/tools/spec-diff-handler.js +3 -3
- package/dist/tools/status-handler.js +6 -1
- package/dist/tools/suggest-mcp-server.js +2 -4
- package/dist/tools/token-recording.d.ts +2 -1
- package/dist/tools/token-recording.js +21 -2
- package/dist/tools/update-status/dod-gates.d.ts +2 -2
- package/dist/tools/update-status/dod-gates.js +47 -52
- package/dist/tools/update-status/evidence-gate.js +6 -4
- package/dist/tools/update-status/index.js +10 -3
- package/dist/tools/update-status/qa-gate.d.ts +5 -0
- package/dist/tools/update-status/qa-gate.js +50 -0
- package/dist/tools/update-status/response-builder.js +96 -2
- package/dist/tools/update-status/transition-guard.js +33 -11
- package/dist/tools/update-status-convention-gate.js +22 -6
- package/dist/tools/validate-team-results.js +23 -1
- package/dist/tools/validate.js +137 -16
- package/dist/transports/http-transport.js +13 -0
- package/dist/transports/transport-factory.js +13 -0
- package/dist/types/qa-gate.d.ts +3 -0
- package/dist/types/tool-classification.d.ts +8 -0
- package/dist/types/tool-classification.js +2 -0
- package/package.json +10 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -10,6 +10,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
10
10
|
import { withEscalation } from '../../engine/escalator/with-escalation.js';
|
|
11
11
|
import { writeImplementationReviewReport } from '../../engine/validator/validation-report-writer.js';
|
|
12
12
|
import { writeSpecReviewFeedback } from '../../engine/validator/spec-review-writer.js';
|
|
13
|
+
import { formatKeyValue } from '../output-formatter.js';
|
|
13
14
|
/**
|
|
14
15
|
* SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
|
|
15
16
|
*
|
|
@@ -54,7 +55,13 @@ export async function runValidateGate(spec, projectPath, forceStatus, forceStatu
|
|
|
54
55
|
catch {
|
|
55
56
|
/* swallow — forced bypass must never throw */
|
|
56
57
|
}
|
|
57
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
blocked: false,
|
|
60
|
+
score: observedScore,
|
|
61
|
+
forced: true,
|
|
62
|
+
forcedReason: forceStatusReason,
|
|
63
|
+
scoreSource: 'forced-best-effort-validateSpec',
|
|
64
|
+
};
|
|
58
65
|
}
|
|
59
66
|
// ---- Normal path — fail-CLOSED --------------------------------------------
|
|
60
67
|
const TIMEOUT_SENTINEL = Symbol('timeout');
|
|
@@ -157,29 +164,36 @@ export async function checkDodGate(spec, specId, projectId, projectPath, _force)
|
|
|
157
164
|
}).catch((err) => {
|
|
158
165
|
console.error('[planu] DoD feedback event failed:', err);
|
|
159
166
|
});
|
|
160
|
-
const failedItems = requiredItems
|
|
161
|
-
.filter((i) => i.status === 'failed')
|
|
162
|
-
.map((i) => ({ id: i.id, description: i.description, category: i.category }));
|
|
163
167
|
return {
|
|
164
168
|
content: [
|
|
165
169
|
{
|
|
166
170
|
type: 'text',
|
|
167
|
-
text:
|
|
168
|
-
|
|
169
|
-
message: humanMessage,
|
|
171
|
+
text: [
|
|
172
|
+
humanMessage,
|
|
170
173
|
expertSummary,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
...fixHints.slice(0, 3),
|
|
175
|
+
...warningItems.slice(0, 2).map((item) => `Warning: ${item}`),
|
|
176
|
+
blockingItems.length > 3 ? `More blockers: ${blockingItems.length - 3}` : undefined,
|
|
177
|
+
warningItems.length > 0 ? `Warnings: ${warningItems.length}` : undefined,
|
|
178
|
+
'Alternatively: use force:true to bypass all gates.',
|
|
179
|
+
]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join('\n'),
|
|
178
182
|
},
|
|
179
183
|
],
|
|
180
184
|
isError: true,
|
|
181
185
|
structuredContent: {
|
|
182
|
-
error: '
|
|
186
|
+
error: 'DoD gate failed',
|
|
187
|
+
errorCode: 'dod_gate_failed',
|
|
188
|
+
message: humanMessage,
|
|
189
|
+
expertSummary,
|
|
190
|
+
blockingItems,
|
|
191
|
+
failedItems: requiredItems
|
|
192
|
+
.filter((i) => i.status === 'failed')
|
|
193
|
+
.map((i) => ({ id: i.id, description: i.description, category: i.category })),
|
|
194
|
+
warningItems,
|
|
195
|
+
fixHints,
|
|
196
|
+
hint: 'Alternatively: use force:true to bypass all gates',
|
|
183
197
|
code: 422,
|
|
184
198
|
context: {
|
|
185
199
|
specId,
|
|
@@ -402,13 +416,14 @@ function validationReportGateError(args) {
|
|
|
402
416
|
content: [
|
|
403
417
|
{
|
|
404
418
|
type: 'text',
|
|
405
|
-
text:
|
|
419
|
+
text: formatKeyValue({
|
|
406
420
|
error: args.error,
|
|
407
421
|
message: args.message,
|
|
408
422
|
artifactPath,
|
|
409
|
-
|
|
423
|
+
failedGates: args.gates.filter((gate) => !gate.passed).length,
|
|
424
|
+
totalGates: args.gates.length,
|
|
410
425
|
fixHint: args.fixHint,
|
|
411
|
-
},
|
|
426
|
+
}, 'Validation report gate failed'),
|
|
412
427
|
},
|
|
413
428
|
],
|
|
414
429
|
isError: true,
|
|
@@ -426,13 +441,14 @@ function specReviewGateError(args) {
|
|
|
426
441
|
content: [
|
|
427
442
|
{
|
|
428
443
|
type: 'text',
|
|
429
|
-
text:
|
|
444
|
+
text: formatKeyValue({
|
|
430
445
|
error: args.error,
|
|
431
446
|
message: args.message,
|
|
432
447
|
artifactPath,
|
|
433
|
-
blockers: args.blockers,
|
|
448
|
+
blockers: args.blockers.length,
|
|
449
|
+
firstBlocker: args.blockers[0],
|
|
434
450
|
fixHint: args.fixHint,
|
|
435
|
-
},
|
|
451
|
+
}, 'Spec review gate failed'),
|
|
436
452
|
},
|
|
437
453
|
],
|
|
438
454
|
isError: true,
|
|
@@ -512,33 +528,7 @@ export async function checkComplianceGate(specId, projectId, projectPath) {
|
|
|
512
528
|
return { skipped: true, score: null, issues: [], blocked: false };
|
|
513
529
|
}
|
|
514
530
|
}
|
|
515
|
-
|
|
516
|
-
export async function checkQaGate(specId, projectPath, force) {
|
|
517
|
-
if (force || !projectPath) {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
const { getGateState } = await import('../../storage/qa-gate-store.js');
|
|
521
|
-
const state = await getGateState(projectPath, specId).catch(() => null);
|
|
522
|
-
if (state?.lastResult?.passed === true) {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
const reason = state === null ? 'QA gate not run' : 'QA gate did not pass';
|
|
526
|
-
return {
|
|
527
|
-
content: [
|
|
528
|
-
{
|
|
529
|
-
type: 'text',
|
|
530
|
-
text: `❌ ${reason} — run the QA checks first or use force:true to bypass`,
|
|
531
|
-
},
|
|
532
|
-
],
|
|
533
|
-
isError: true,
|
|
534
|
-
structuredContent: {
|
|
535
|
-
error: 'qa_gate_not_passed',
|
|
536
|
-
code: 422,
|
|
537
|
-
context: { specId, reason },
|
|
538
|
-
fixHint: 'Run the QA gate and record the result: typecheck + test by default, or typecheck + test:coverage when the spec declares a coverage threshold. Then retry update_status(done). Or use force:true to bypass.',
|
|
539
|
-
},
|
|
540
|
-
};
|
|
541
|
-
}
|
|
531
|
+
export { checkQaGate } from './qa-gate.js';
|
|
542
532
|
/**
|
|
543
533
|
* SPEC-716 / SPEC-780: Format gate for transition to 'approved'.
|
|
544
534
|
*
|
|
@@ -576,17 +566,22 @@ export async function checkApprovedFormatGate(spec, newStatus, forceApprove = fa
|
|
|
576
566
|
content: [
|
|
577
567
|
{
|
|
578
568
|
type: 'text',
|
|
579
|
-
text:
|
|
569
|
+
text: formatKeyValue({
|
|
580
570
|
error: 'SPEC_FORMAT_INVALID',
|
|
581
571
|
message: `Spec format validation failed with ${String(result.errors.length)} error(s) — fix before approving, or pass forceApprove: true to bypass with warnings.`,
|
|
582
|
-
errors: result.errors,
|
|
583
|
-
warnings: result.warnings,
|
|
584
|
-
|
|
572
|
+
errors: result.errors.length,
|
|
573
|
+
warnings: result.warnings.length,
|
|
574
|
+
firstError: result.errors[0]?.message,
|
|
575
|
+
firstErrorPath: result.errors[0]?.path,
|
|
576
|
+
}, 'Spec format invalid'),
|
|
585
577
|
},
|
|
586
578
|
],
|
|
587
579
|
isError: true,
|
|
588
580
|
structuredContent: {
|
|
589
581
|
error: 'SPEC_FORMAT_INVALID',
|
|
582
|
+
message: `Spec format validation failed with ${String(result.errors.length)} error(s) — fix before approving, or pass forceApprove: true to bypass with warnings.`,
|
|
583
|
+
errors: result.errors,
|
|
584
|
+
warnings: result.warnings,
|
|
590
585
|
code: 422,
|
|
591
586
|
context: {
|
|
592
587
|
specId: spec.id,
|
|
@@ -7,6 +7,7 @@ import { buildSpecEvidenceIndex } from '../../engine/evidence-index/index-builde
|
|
|
7
7
|
import { checkDoneDriftContract } from '../../engine/evidence-index/done-drift.js';
|
|
8
8
|
import { parseTechnicalReferenceGroundingRecords } from '../../engine/spec-grounding/contract.js';
|
|
9
9
|
import { extractAcceptanceCriteriaTexts } from '../../engine/spec-format/acceptance-criteria.js';
|
|
10
|
+
import { formatKeyValue } from '../output-formatter.js';
|
|
10
11
|
/** SPEC-1054: BDD/SDD lifecycle evidence gate. */
|
|
11
12
|
export async function checkLifecycleEvidenceTransitionGate(args) {
|
|
12
13
|
const [criteriaResult, artifacts] = await Promise.all([
|
|
@@ -69,15 +70,16 @@ function evidenceGateError(args) {
|
|
|
69
70
|
content: [
|
|
70
71
|
{
|
|
71
72
|
type: 'text',
|
|
72
|
-
text:
|
|
73
|
+
text: formatKeyValue({
|
|
73
74
|
error: 'lifecycle_evidence_gate_failed',
|
|
74
75
|
message: `BDD/SDD evidence gate blocked ${args.transition}.`,
|
|
75
76
|
specId: args.specId,
|
|
76
77
|
transition: args.transition,
|
|
77
|
-
issues: args.issues,
|
|
78
|
-
|
|
78
|
+
issues: args.issues.length,
|
|
79
|
+
firstIssue: args.issues[0]?.message,
|
|
80
|
+
requiredContractKinds: args.requiredContractKinds.length,
|
|
79
81
|
fixHint: 'Add the missing evidence artifact to the external Planu project data handoff store, then retry the transition. Do not create JSON files under planu/specs/<spec>/.',
|
|
80
|
-
}
|
|
82
|
+
}),
|
|
81
83
|
},
|
|
82
84
|
],
|
|
83
85
|
isError: true,
|
|
@@ -26,6 +26,7 @@ import { updateFrontmatterField } from '../../engine/frontmatter-parser.js';
|
|
|
26
26
|
import { atomicWriteFile } from '../../engine/safety/atomic-write-file.js';
|
|
27
27
|
import { recordForceUsage } from '../../storage/force-analytics-store.js';
|
|
28
28
|
import { maybeSafePushOnDone, runCascadeForResponse } from './side-effects.js';
|
|
29
|
+
import { formatKeyValue } from '../output-formatter.js';
|
|
29
30
|
import { checkCodeReality } from '../../engine/code-scanner/index.js';
|
|
30
31
|
import { scanCrashRisks } from '../../engine/crash-shield/index.js';
|
|
31
32
|
import { shouldSkipCrashScan, recordCrashScanRun, } from '../../engine/autopilot/crash-shield-rate-limiter.js';
|
|
@@ -341,11 +342,11 @@ export async function handleUpdateStatus(params, server) {
|
|
|
341
342
|
content: [
|
|
342
343
|
{
|
|
343
344
|
type: 'text',
|
|
344
|
-
text:
|
|
345
|
+
text: formatKeyValue({
|
|
345
346
|
error: 'invalid_input',
|
|
346
347
|
message: reverseValidation.error,
|
|
347
348
|
fixHint: reverseValidation.fixHint,
|
|
348
|
-
}
|
|
349
|
+
}),
|
|
349
350
|
},
|
|
350
351
|
],
|
|
351
352
|
isError: true,
|
|
@@ -466,7 +467,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
466
467
|
// SPEC-642: QA gate — block done if typecheck + test:coverage have not passed
|
|
467
468
|
// ---------------------------------------------------------------------------
|
|
468
469
|
if (newStatus === 'done') {
|
|
469
|
-
const qaGateResult = await checkQaGate(
|
|
470
|
+
const qaGateResult = await checkQaGate(spec, effectiveGatePath, params.force ?? false);
|
|
470
471
|
if (qaGateResult !== null) {
|
|
471
472
|
return qaGateResult;
|
|
472
473
|
}
|
|
@@ -503,6 +504,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
503
504
|
// SPEC-447: Compliance gate on 'review' or 'implementing'
|
|
504
505
|
// ---------------------------------------------------------------------------
|
|
505
506
|
let validateScore = null;
|
|
507
|
+
let validateScoreSource = null;
|
|
506
508
|
let crashShieldWarning = null;
|
|
507
509
|
let crashShieldSkipReason = null;
|
|
508
510
|
let complianceGateResult = null;
|
|
@@ -537,11 +539,15 @@ export async function handleUpdateStatus(params, server) {
|
|
|
537
539
|
}
|
|
538
540
|
validateScore = validateGateResult.score;
|
|
539
541
|
if (validateGateResult.forced) {
|
|
542
|
+
validateScoreSource = validateGateResult.scoreSource;
|
|
540
543
|
forcedValidateBypass = {
|
|
541
544
|
reason: validateGateResult.forcedReason,
|
|
542
545
|
observedScore: validateGateResult.score,
|
|
543
546
|
};
|
|
544
547
|
}
|
|
548
|
+
else {
|
|
549
|
+
validateScoreSource = 'validateSpec';
|
|
550
|
+
}
|
|
545
551
|
}
|
|
546
552
|
if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
|
|
547
553
|
const validationReportError = await checkValidationReportGate(specId, projectId, false);
|
|
@@ -1045,6 +1051,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1045
1051
|
forcedBypassWarning,
|
|
1046
1052
|
forceAnalyticsWarning,
|
|
1047
1053
|
validateScore,
|
|
1054
|
+
validateScoreSource,
|
|
1048
1055
|
// SPEC-721: audit ID for the forced validate bypass (null when no bypass occurred)
|
|
1049
1056
|
forceStatusAuditId,
|
|
1050
1057
|
// SPEC-780: audit ID for the forced approve bypass (null when no bypass occurred)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ToolResult } from '../../types/index.js';
|
|
2
|
+
import type { Spec } from '../../types/spec/core.js';
|
|
3
|
+
/** SPEC-642 / SPEC-1111: verify and persist local QA evidence before done. */
|
|
4
|
+
export declare function checkQaGate(spec: Spec, projectPath: string | undefined, force: boolean): Promise<ToolResult | null>;
|
|
5
|
+
//# sourceMappingURL=qa-gate.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** SPEC-642 / SPEC-1111: verify and persist local QA evidence before done. */
|
|
2
|
+
export async function checkQaGate(spec, projectPath, force) {
|
|
3
|
+
if (force || !projectPath) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const specId = spec.id;
|
|
7
|
+
const { getGateState, saveGateState } = await import('../../storage/qa-gate-store.js');
|
|
8
|
+
const state = await getGateState(projectPath, specId).catch(() => null);
|
|
9
|
+
if (state?.lastResult?.passed === true) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
let effectiveResult = state?.lastResult ?? null;
|
|
13
|
+
if (effectiveResult === null) {
|
|
14
|
+
const { runQaGate } = await import('../../engine/qa-gate.js');
|
|
15
|
+
effectiveResult = await runQaGate(spec, projectPath);
|
|
16
|
+
await saveGateState(projectPath, specId, effectiveResult);
|
|
17
|
+
}
|
|
18
|
+
if (effectiveResult.passed) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const failedChecks = effectiveResult.checks.filter((check) => !check.passed);
|
|
22
|
+
const failedSummary = failedChecks.length > 0
|
|
23
|
+
? failedChecks
|
|
24
|
+
.map((check) => {
|
|
25
|
+
const command = check.command ?? check.name;
|
|
26
|
+
const reason = check.timedOut
|
|
27
|
+
? `timed out after ${String(check.durationMs)}ms`
|
|
28
|
+
: (check.errorMessage ?? 'exited with a non-zero status');
|
|
29
|
+
return `${command}: ${reason}`;
|
|
30
|
+
})
|
|
31
|
+
.join('; ')
|
|
32
|
+
: 'No failed check details recorded.';
|
|
33
|
+
const reason = 'QA gate did not pass';
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: `❌ ${reason} — ${failedSummary}. Fix the failing local QA checks or use force:true to bypass`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
isError: true,
|
|
42
|
+
structuredContent: {
|
|
43
|
+
error: 'qa_gate_not_passed',
|
|
44
|
+
code: 422,
|
|
45
|
+
context: { specId, reason, failedChecks },
|
|
46
|
+
fixHint: 'Fix the failing QA command(s), rerun update_status(done), or use force:true to bypass with audit context.',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=qa-gate.js.map
|
|
@@ -116,7 +116,10 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
116
116
|
lines.push(`\n🤖 Tokens: ${tokenStr}${costStr}${r.actuals.estimated ? ' *(auto-estimated)*' : ''}`);
|
|
117
117
|
}
|
|
118
118
|
if (r.validateScore !== null && r.validateScore !== undefined) {
|
|
119
|
-
|
|
119
|
+
const sourceSuffix = r.validateScoreSource === 'forced-best-effort-validateSpec'
|
|
120
|
+
? ' (forced best-effort observation from validateSpec)'
|
|
121
|
+
: '';
|
|
122
|
+
lines.push(`\n✅ Validate score: ${String(r.validateScore)}/100${sourceSuffix}`);
|
|
120
123
|
}
|
|
121
124
|
if (r.cascade.affectedSpecs.length > 0) {
|
|
122
125
|
lines.push(`\n🔗 Downstream specs affected: ${r.cascade.affectedSpecs.map((s) => `SPEC-${s}`).join(', ')}`);
|
|
@@ -201,12 +204,103 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
201
204
|
: withNextSteps;
|
|
202
205
|
const displayTitle = specTitle ?? `SPEC-${specId}`;
|
|
203
206
|
const humanSummary = buildUpdateStatusSummary(displayTitle, newStatus);
|
|
207
|
+
const warningItems = [
|
|
208
|
+
r.protectedBranchWarning,
|
|
209
|
+
r.mergeWarning,
|
|
210
|
+
r.validationWarning,
|
|
211
|
+
r.codeRealityWarning,
|
|
212
|
+
r.crashShieldWarning,
|
|
213
|
+
r.forcedBypassWarning,
|
|
214
|
+
...(r.constitutionWarnings ?? []).map((w) => w.description),
|
|
215
|
+
...(r.conventionWarnings ?? []),
|
|
216
|
+
...(r.lintWarnings ?? []),
|
|
217
|
+
...(r.testWarnings ?? []),
|
|
218
|
+
...(r.compileWarnings ?? []),
|
|
219
|
+
...(r.frontmatterSyncWarnings ?? []).map((w) => `frontmatter sync failed for ${w.specId}: ${w.reason}`),
|
|
220
|
+
].filter((item) => typeof item === 'string' && item.length > 0);
|
|
221
|
+
const validationIssues = [
|
|
222
|
+
...(r.validationWarning ? [r.validationWarning] : []),
|
|
223
|
+
...(r.complianceGateResult?.issues ?? []),
|
|
224
|
+
...(r.lintWarnings ?? []),
|
|
225
|
+
...(r.testWarnings ?? []),
|
|
226
|
+
...(r.compileWarnings ?? []),
|
|
227
|
+
];
|
|
228
|
+
const structuredContent = {
|
|
229
|
+
specId,
|
|
230
|
+
previousStatus: currentStatus,
|
|
231
|
+
newStatus,
|
|
232
|
+
autoBranch: r.autoBranch ?? undefined,
|
|
233
|
+
protectedBranchWarning: r.protectedBranchWarning ?? undefined,
|
|
234
|
+
mergeWarning: r.mergeWarning ?? undefined,
|
|
235
|
+
validationWarning: r.validationWarning ?? undefined,
|
|
236
|
+
prSuggestion: r.prSuggestion ?? null,
|
|
237
|
+
metrics: r.metrics ?? undefined,
|
|
238
|
+
actuals: r.actuals ?? undefined,
|
|
239
|
+
cascade: {
|
|
240
|
+
...(result.cascade ?? {}),
|
|
241
|
+
affectedSpecs: r.cascade.affectedSpecs,
|
|
242
|
+
},
|
|
243
|
+
constitutionWarnings: r.constitutionWarnings?.slice(0, 5),
|
|
244
|
+
conventionWarnings: r.conventionWarnings?.slice(0, 5),
|
|
245
|
+
lintWarnings: r.lintWarnings?.slice(0, 5),
|
|
246
|
+
testWarnings: r.testWarnings?.slice(0, 5),
|
|
247
|
+
compileWarnings: r.compileWarnings?.slice(0, 5),
|
|
248
|
+
validateScore: r.validateScore ?? undefined,
|
|
249
|
+
validateScoreSource: r.validateScoreSource ?? undefined,
|
|
250
|
+
complianceGateResult: r.complianceGateResult
|
|
251
|
+
? {
|
|
252
|
+
skipped: r.complianceGateResult.skipped,
|
|
253
|
+
score: r.complianceGateResult.score,
|
|
254
|
+
blocked: r.complianceGateResult.blocked,
|
|
255
|
+
issues: r.complianceGateResult.issues.slice(0, 5),
|
|
256
|
+
}
|
|
257
|
+
: undefined,
|
|
258
|
+
forceApproveAuditId: typeof result.forceApproveAuditId === 'string' ? result.forceApproveAuditId : undefined,
|
|
259
|
+
forceStatusAuditId: typeof result.forceStatusAuditId === 'string' ? result.forceStatusAuditId : undefined,
|
|
260
|
+
qualityWarnings: Array.isArray(result.qualityWarnings)
|
|
261
|
+
? result.qualityWarnings.slice(0, 5)
|
|
262
|
+
: undefined,
|
|
263
|
+
interactiveQuestions: Array.isArray(result.interactiveQuestions)
|
|
264
|
+
? result.interactiveQuestions
|
|
265
|
+
: undefined,
|
|
266
|
+
transitionSummary: {
|
|
267
|
+
title: displayTitle,
|
|
268
|
+
from: currentStatus,
|
|
269
|
+
to: newStatus,
|
|
270
|
+
autoBranch: r.autoBranch ?? undefined,
|
|
271
|
+
versionSnapshotTag: r.versionSnapshotTag ?? undefined,
|
|
272
|
+
},
|
|
273
|
+
validationSummary: {
|
|
274
|
+
validateScore: r.validateScore ?? null,
|
|
275
|
+
validateScoreSource: r.validateScoreSource ?? null,
|
|
276
|
+
complianceScore: r.complianceGateResult?.score ?? null,
|
|
277
|
+
complianceBlocked: r.complianceGateResult?.blocked ?? false,
|
|
278
|
+
validationIssuesCount: validationIssues.length,
|
|
279
|
+
validationIssuesTop5: validationIssues.slice(0, 5),
|
|
280
|
+
},
|
|
281
|
+
blockersCount: validationIssues.length,
|
|
282
|
+
blockersTop3: validationIssues.slice(0, 3),
|
|
283
|
+
warningsCount: warningItems.length,
|
|
284
|
+
warningsTop5: warningItems.slice(0, 5),
|
|
285
|
+
affectedSpecsCount: r.cascade.affectedSpecs.length,
|
|
286
|
+
affectedSpecsTop5: r.cascade.affectedSpecs.slice(0, 5),
|
|
287
|
+
nextAction,
|
|
288
|
+
lifecycleMap,
|
|
289
|
+
humanSummary,
|
|
290
|
+
orchestrationPlan: r.orchestrationPlan
|
|
291
|
+
? {
|
|
292
|
+
waves: r.orchestrationPlan.executionOrder.length,
|
|
293
|
+
parallelSafetyVerdict: r.orchestrationPlan.parallelSafetyVerdict,
|
|
294
|
+
executionOrder: r.orchestrationPlan.executionOrder,
|
|
295
|
+
}
|
|
296
|
+
: undefined,
|
|
297
|
+
};
|
|
204
298
|
return {
|
|
205
299
|
content: [
|
|
206
300
|
{ type: 'text', text: markdownText },
|
|
207
301
|
{ type: 'text', text: humanSummary },
|
|
208
302
|
],
|
|
209
|
-
structuredContent
|
|
303
|
+
structuredContent,
|
|
210
304
|
};
|
|
211
305
|
}
|
|
212
306
|
/** Render a compact lifecycle map line showing current, next, and remaining steps */
|
|
@@ -8,6 +8,7 @@ import { checkReadinessInternal } from '../../engine/readiness-checker.js';
|
|
|
8
8
|
import { validateEnglishOnlySpecText } from '../../engine/spec-language/english-only.js';
|
|
9
9
|
import { checkGroundedSpecContract } from '../../engine/spec-grounding/contract.js';
|
|
10
10
|
import { checkGenericSpecOutput } from '../../engine/spec-quality/generic-output-gate.js';
|
|
11
|
+
import { formatKeyValue } from '../output-formatter.js';
|
|
11
12
|
/**
|
|
12
13
|
* Valid state transitions for spec lifecycle.
|
|
13
14
|
* draft -> review -> approved -> implementing -> done
|
|
@@ -133,16 +134,29 @@ export function checkDorGate(spec, specId, projectId, newStatus) {
|
|
|
133
134
|
content: [
|
|
134
135
|
{
|
|
135
136
|
type: 'text',
|
|
136
|
-
text:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
text: [
|
|
138
|
+
dorResult.humanMessage,
|
|
139
|
+
dorResult.expertSummary,
|
|
140
|
+
...dorResult.blockingItems.slice(0, 3).map((item) => `Blocker: ${item}`),
|
|
141
|
+
dorResult.blockingItems.length > 3
|
|
142
|
+
? `More blockers: ${String(dorResult.blockingItems.length - 3)}`
|
|
143
|
+
: undefined,
|
|
144
|
+
dorResult.warningItems.length > 0
|
|
145
|
+
? `Warnings: ${String(dorResult.warningItems.length)}`
|
|
146
|
+
: undefined,
|
|
147
|
+
]
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join('\n'),
|
|
143
150
|
},
|
|
144
151
|
],
|
|
145
152
|
isError: true,
|
|
153
|
+
structuredContent: {
|
|
154
|
+
error: 'DoR gate failed',
|
|
155
|
+
message: dorResult.humanMessage,
|
|
156
|
+
expertSummary: dorResult.expertSummary,
|
|
157
|
+
blockingItems: dorResult.blockingItems,
|
|
158
|
+
warningItems: dorResult.warningItems,
|
|
159
|
+
},
|
|
146
160
|
};
|
|
147
161
|
}
|
|
148
162
|
return null;
|
|
@@ -163,16 +177,24 @@ export async function checkAmbiguityGate(spec, newStatus) {
|
|
|
163
177
|
content: [
|
|
164
178
|
{
|
|
165
179
|
type: 'text',
|
|
166
|
-
text:
|
|
180
|
+
text: formatKeyValue({
|
|
167
181
|
error: 'Ambiguity gate failed',
|
|
168
182
|
message: `Ambiguity score ${String(report.score)}/100 — below 70. Fix vague terms in criteria before approving.`,
|
|
169
183
|
ambiguityScore: report.score,
|
|
170
|
-
violations: report.violations.
|
|
171
|
-
blockers: report.blockers.
|
|
172
|
-
|
|
184
|
+
violations: report.violations.length,
|
|
185
|
+
blockers: report.blockers.length,
|
|
186
|
+
firstBlocker: report.blockers[0],
|
|
187
|
+
}, 'Ambiguity gate failed'),
|
|
173
188
|
},
|
|
174
189
|
],
|
|
175
190
|
isError: true,
|
|
191
|
+
structuredContent: {
|
|
192
|
+
error: 'Ambiguity gate failed',
|
|
193
|
+
message: `Ambiguity score ${String(report.score)}/100 — below 70. Fix vague terms in criteria before approving.`,
|
|
194
|
+
ambiguityScore: report.score,
|
|
195
|
+
violations: report.violations.slice(0, 5),
|
|
196
|
+
blockers: report.blockers.slice(0, 5),
|
|
197
|
+
},
|
|
176
198
|
};
|
|
177
199
|
}
|
|
178
200
|
/**
|
|
@@ -22,7 +22,7 @@ async function execGate(cmd, cwd, timeoutMs) {
|
|
|
22
22
|
timeout: timeoutMs,
|
|
23
23
|
maxBuffer: 4 * 1024 * 1024,
|
|
24
24
|
});
|
|
25
|
-
return { output: stdout.trim(), ok: true };
|
|
25
|
+
return { output: stdout.trim(), ok: true, timedOut: false };
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
28
28
|
const e = err;
|
|
@@ -31,7 +31,8 @@ async function execGate(cmd, cwd, timeoutMs) {
|
|
|
31
31
|
: typeof e.stdout === 'string'
|
|
32
32
|
? e.stdout
|
|
33
33
|
: '';
|
|
34
|
-
|
|
34
|
+
const timedOut = e.killed === true || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT';
|
|
35
|
+
return { output: raw.trim(), ok: false, timedOut, errorMessage: e.message };
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
function countLintIssues(output) {
|
|
@@ -87,23 +88,38 @@ async function checkTestGate(projectPath, testCmd) {
|
|
|
87
88
|
}
|
|
88
89
|
// Run tests with a reasonable timeout — only a warning if they fail
|
|
89
90
|
const cmd = testCmd ?? 'npx vitest run --reporter=verbose';
|
|
90
|
-
const
|
|
91
|
+
const timeoutMs = 120_000;
|
|
92
|
+
const { output, ok, timedOut, errorMessage } = await execGate(cmd, projectPath, timeoutMs);
|
|
91
93
|
if (ok) {
|
|
92
94
|
return [];
|
|
93
95
|
}
|
|
96
|
+
if (timedOut) {
|
|
97
|
+
return [`Tests: timed out after ${String(timeoutMs)}ms. Command: ${cmd}`];
|
|
98
|
+
}
|
|
94
99
|
// Extract summary line (e.g. "5 failed" or "FAIL src/foo.test.ts")
|
|
95
100
|
const failLine = output
|
|
96
101
|
.split('\n')
|
|
97
102
|
.reverse()
|
|
98
|
-
.find((l) =>
|
|
103
|
+
.find((l) => isFailureSummaryLine(l));
|
|
99
104
|
const summary = failLine ??
|
|
100
105
|
output
|
|
101
106
|
.split('\n')
|
|
102
|
-
.filter((l) => l.trim())
|
|
107
|
+
.filter((l) => l.trim() && !isPassingSummaryLine(l))
|
|
103
108
|
.slice(-1)[0] ??
|
|
104
|
-
|
|
109
|
+
errorMessage ??
|
|
110
|
+
'No failure summary available.';
|
|
105
111
|
return [`Tests: failed. ${summary.trim()}`];
|
|
106
112
|
}
|
|
113
|
+
function isPassingSummaryLine(line) {
|
|
114
|
+
return /^\s*(?:✓|PASS\b|Test Files\s+\d+\s+passed|Tests\s+\d+\s+passed)/i.test(line.trim());
|
|
115
|
+
}
|
|
116
|
+
function isFailureSummaryLine(line) {
|
|
117
|
+
const trimmed = line.trim();
|
|
118
|
+
if (trimmed.length === 0 || isPassingSummaryLine(trimmed)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return /\b(?:FAIL|failed|failing|error|errored)\b/i.test(trimmed);
|
|
122
|
+
}
|
|
107
123
|
// ---------------------------------------------------------------------------
|
|
108
124
|
// Convention gate (existing logic, unchanged)
|
|
109
125
|
// ---------------------------------------------------------------------------
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import { specStore } from '../storage/index.js';
|
|
2
2
|
import { validateTeamResults } from '../engine/team-planner/index.js';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { formatKeyValue } from './output-formatter.js';
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Helpers
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
function text(content) {
|
|
8
|
-
|
|
9
|
+
const conflicts = Array.isArray(content.conflicts) ? content.conflicts.length : undefined;
|
|
10
|
+
const mergeOrder = Array.isArray(content.mergeOrder) ? content.mergeOrder.length : undefined;
|
|
11
|
+
const mergeOrderPreview = Array.isArray(content.mergeOrder)
|
|
12
|
+
? content.mergeOrder.slice(0, 5).join(', ')
|
|
13
|
+
: undefined;
|
|
14
|
+
const warnings = Array.isArray(content.warnings) ? content.warnings.length : undefined;
|
|
15
|
+
return {
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'text',
|
|
19
|
+
text: formatKeyValue({
|
|
20
|
+
passed: content.passed,
|
|
21
|
+
conflicts,
|
|
22
|
+
mergeOrder,
|
|
23
|
+
mergeOrderPreview,
|
|
24
|
+
warnings,
|
|
25
|
+
summary: content.summary,
|
|
26
|
+
}) + '\nVerification commands: pnpm typecheck; pnpm lint; pnpm test',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
structuredContent: content,
|
|
30
|
+
};
|
|
9
31
|
}
|
|
10
32
|
function err(message) {
|
|
11
33
|
return { content: [{ type: 'text', text: message }], isError: true };
|