@planu/cli 4.10.6 → 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 +11 -1
- package/dist/engine/local-first/tool-classification.d.ts +1 -0
- package/dist/engine/local-first/tool-classification.js +294 -100
- 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/orchestrate-locking.js +15 -4
- 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/suggest-mcp-server.js +2 -4
- package/dist/tools/update-status/dod-gates.js +39 -24
- package/dist/tools/update-status/evidence-gate.js +6 -4
- package/dist/tools/update-status/index.js +3 -2
- package/dist/tools/update-status/transition-guard.js +33 -11
- package/dist/tools/validate-team-results.js +23 -1
- package/dist/transports/http-transport.js +13 -0
- package/package.json +10 -10
- package/planu-native.json +29 -8
- package/planu-plugin.json +35 -7
|
@@ -3,6 +3,7 @@ import { specStore, knowledgeStore } from '../../storage/index.js';
|
|
|
3
3
|
import { ti } from '../../i18n/index.js';
|
|
4
4
|
import { detectPIIInSpec, detectLegalFramework, generateRetentionPolicy, generatePrivacySection, } from '../../engine/pii-detector.js';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { formatKeyValue } from '../output-formatter.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// detect subcommand
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -78,15 +79,24 @@ export async function handleDetect(specId, projectId, inlineContent, projectPath
|
|
|
78
79
|
},
|
|
79
80
|
{
|
|
80
81
|
type: 'text',
|
|
81
|
-
text:
|
|
82
|
+
text: formatKeyValue({
|
|
82
83
|
subcommand: 'detect',
|
|
83
|
-
|
|
84
|
+
hasPII: result.hasPII,
|
|
85
|
+
piiFields: result.fields.length,
|
|
86
|
+
sensitiveSpecialCount: result.sensitiveSpecialCount,
|
|
84
87
|
framework,
|
|
85
|
-
retentionPolicy,
|
|
86
|
-
requirement,
|
|
87
|
-
}
|
|
88
|
+
retentionPolicy: 'generated',
|
|
89
|
+
challengeRequired: requirement.challengeRequired,
|
|
90
|
+
}),
|
|
88
91
|
},
|
|
89
92
|
],
|
|
93
|
+
structuredContent: {
|
|
94
|
+
subcommand: 'detect',
|
|
95
|
+
detectionResult: result,
|
|
96
|
+
framework,
|
|
97
|
+
retentionPolicy,
|
|
98
|
+
requirement,
|
|
99
|
+
},
|
|
90
100
|
};
|
|
91
101
|
}
|
|
92
102
|
// ---------------------------------------------------------------------------
|
|
@@ -115,21 +125,28 @@ export function handleRetentionPolicy(modelName, projectPath) {
|
|
|
115
125
|
},
|
|
116
126
|
{
|
|
117
127
|
type: 'text',
|
|
118
|
-
text:
|
|
128
|
+
text: formatKeyValue({
|
|
119
129
|
subcommand: 'retention-policy',
|
|
120
130
|
modelName: effectiveModel,
|
|
121
131
|
framework,
|
|
122
|
-
retentionPolicy:
|
|
123
|
-
auditFields:
|
|
124
|
-
|
|
125
|
-
'updated_at',
|
|
126
|
-
'deleted_at',
|
|
127
|
-
'data_owner_id',
|
|
128
|
-
'retention_expires_at',
|
|
129
|
-
],
|
|
130
|
-
}, null, 2),
|
|
132
|
+
retentionPolicy: 'generated',
|
|
133
|
+
auditFields: 5,
|
|
134
|
+
}),
|
|
131
135
|
},
|
|
132
136
|
],
|
|
137
|
+
structuredContent: {
|
|
138
|
+
subcommand: 'retention-policy',
|
|
139
|
+
modelName: effectiveModel,
|
|
140
|
+
framework,
|
|
141
|
+
retentionPolicy: defaultRetentionPolicy,
|
|
142
|
+
auditFields: [
|
|
143
|
+
'created_at',
|
|
144
|
+
'updated_at',
|
|
145
|
+
'deleted_at',
|
|
146
|
+
'data_owner_id',
|
|
147
|
+
'retention_expires_at',
|
|
148
|
+
],
|
|
149
|
+
},
|
|
133
150
|
};
|
|
134
151
|
}
|
|
135
152
|
// Re-export for use by the main handler
|
|
@@ -172,10 +172,6 @@ export async function handleDefineUIContract(args) {
|
|
|
172
172
|
type: 'text',
|
|
173
173
|
text: ti('tools.define_ui_contract.success', { componentCount: String(totalComponents) }),
|
|
174
174
|
},
|
|
175
|
-
{
|
|
176
|
-
type: 'text',
|
|
177
|
-
text: JSON.stringify(contract, null, 2),
|
|
178
|
-
},
|
|
179
175
|
{
|
|
180
176
|
type: 'text',
|
|
181
177
|
text: formatStylingContract(stylingContract),
|
|
@@ -189,6 +185,7 @@ export async function handleDefineUIContract(args) {
|
|
|
189
185
|
text: formatA11yContract(a11yContract),
|
|
190
186
|
},
|
|
191
187
|
],
|
|
188
|
+
structuredContent: contract,
|
|
192
189
|
};
|
|
193
190
|
}
|
|
194
191
|
// --- Project context inference ---
|
|
@@ -194,11 +194,8 @@ export async function handleDesignSchema(args) {
|
|
|
194
194
|
indexCount: String(indexes.length),
|
|
195
195
|
}),
|
|
196
196
|
},
|
|
197
|
-
{
|
|
198
|
-
type: 'text',
|
|
199
|
-
text: JSON.stringify(schema, null, 2),
|
|
200
|
-
},
|
|
201
197
|
],
|
|
198
|
+
structuredContent: schema,
|
|
202
199
|
};
|
|
203
200
|
}
|
|
204
201
|
// -- SPEC-059: Specialized DB schema dispatch ---------------------------------
|
|
@@ -78,6 +78,15 @@ async function handleDetectDriftInner(args) {
|
|
|
78
78
|
// 5. Check PLAN.md progress if spec has a plan
|
|
79
79
|
const planProgress = await loadPlanProgress(spec.planPath);
|
|
80
80
|
const statusEmoji = report.isCompliant ? 'PASS' : 'FAIL';
|
|
81
|
+
const constitutionCompliance = constitution
|
|
82
|
+
? {
|
|
83
|
+
totalPrinciples: constitution.principles.length,
|
|
84
|
+
strictCount: constitution.principles.filter((p) => p.enforceLevel === 'strict').length,
|
|
85
|
+
checkedAt: new Date().toISOString(),
|
|
86
|
+
}
|
|
87
|
+
: undefined;
|
|
88
|
+
const criticalPrivacyDriftCount = privacyDrifts.filter((d) => d.severity === 'critical').length;
|
|
89
|
+
const breakingEventSchemaDriftCount = eventSchemaDrifts.filter((d) => d.severity === 'breaking').length;
|
|
81
90
|
// Dispatch on_drift_detected hook event when drift is found (fire-and-forget)
|
|
82
91
|
if (!report.isCompliant) {
|
|
83
92
|
void dispatchHookEvent(projectId, buildDriftDetectedPayload(projectId, specId, report.driftScore, `${report.drifts.length} drift(s) detected`)).catch((err) => {
|
|
@@ -95,20 +104,20 @@ async function handleDetectDriftInner(args) {
|
|
|
95
104
|
text: formattedOutput,
|
|
96
105
|
},
|
|
97
106
|
...(planProgress !== null
|
|
98
|
-
? [
|
|
107
|
+
? [
|
|
108
|
+
{
|
|
109
|
+
type: 'text',
|
|
110
|
+
text: `Plan progress: ${Object.entries(planProgress)
|
|
111
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
112
|
+
.join(', ')}`,
|
|
113
|
+
},
|
|
114
|
+
]
|
|
99
115
|
: []),
|
|
100
116
|
...(constitution
|
|
101
117
|
? [
|
|
102
118
|
{
|
|
103
119
|
type: 'text',
|
|
104
|
-
text:
|
|
105
|
-
constitutionCompliance: {
|
|
106
|
-
totalPrinciples: constitution.principles.length,
|
|
107
|
-
strictCount: constitution.principles.filter((p) => p.enforceLevel === 'strict')
|
|
108
|
-
.length,
|
|
109
|
-
checkedAt: new Date().toISOString(),
|
|
110
|
-
},
|
|
111
|
-
}, null, 2),
|
|
120
|
+
text: `Constitution compliance: ${constitution.principles.length} principles, ${constitutionCompliance?.strictCount ?? 0} strict`,
|
|
112
121
|
},
|
|
113
122
|
]
|
|
114
123
|
: []),
|
|
@@ -117,14 +126,11 @@ async function handleDetectDriftInner(args) {
|
|
|
117
126
|
? [
|
|
118
127
|
{
|
|
119
128
|
type: 'text',
|
|
120
|
-
text:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
message: 'Privacy drift detected. Review missing privacy criteria before merging.',
|
|
126
|
-
},
|
|
127
|
-
}, null, 2),
|
|
129
|
+
text: `Privacy drift: ${privacyDrifts.length} issue(s), ${criticalPrivacyDriftCount} critical. ` +
|
|
130
|
+
`Types: ${privacyDrifts
|
|
131
|
+
.map((drift) => drift.driftType)
|
|
132
|
+
.slice(0, 5)
|
|
133
|
+
.join(', ')}. Review missing privacy criteria before merging.`,
|
|
128
134
|
},
|
|
129
135
|
]
|
|
130
136
|
: []),
|
|
@@ -133,21 +139,35 @@ async function handleDetectDriftInner(args) {
|
|
|
133
139
|
? [
|
|
134
140
|
{
|
|
135
141
|
type: 'text',
|
|
136
|
-
text:
|
|
137
|
-
eventSchemaDrift: {
|
|
138
|
-
type: 'event-schema-mismatch',
|
|
139
|
-
count: eventSchemaDrifts.length,
|
|
140
|
-
/* v8 ignore next 2 */
|
|
141
|
-
breakingCount: eventSchemaDrifts.filter((d) => d.severity === 'breaking')
|
|
142
|
-
.length,
|
|
143
|
-
drifts: eventSchemaDrifts,
|
|
144
|
-
message: 'Event schema drift detected. Implementation diverges from versioned contract.',
|
|
145
|
-
},
|
|
146
|
-
}, null, 2),
|
|
142
|
+
text: `Event schema drift: ${eventSchemaDrifts.length} mismatch(es), ${breakingEventSchemaDriftCount} breaking. Implementation diverges from versioned contract.`,
|
|
147
143
|
},
|
|
148
144
|
]
|
|
149
145
|
: []),
|
|
150
146
|
],
|
|
147
|
+
structuredContent: {
|
|
148
|
+
drift: report,
|
|
149
|
+
...(planProgress !== null ? { planProgress } : {}),
|
|
150
|
+
...(constitutionCompliance ? { constitutionCompliance } : {}),
|
|
151
|
+
...(privacyDrifts.length > 0
|
|
152
|
+
? {
|
|
153
|
+
privacyDrift: {
|
|
154
|
+
count: privacyDrifts.length,
|
|
155
|
+
criticalCount: criticalPrivacyDriftCount,
|
|
156
|
+
drifts: privacyDrifts,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
...(eventSchemaDrifts.length > 0
|
|
161
|
+
? {
|
|
162
|
+
eventSchemaDrift: {
|
|
163
|
+
type: 'event-schema-mismatch',
|
|
164
|
+
count: eventSchemaDrifts.length,
|
|
165
|
+
breakingCount: breakingEventSchemaDriftCount,
|
|
166
|
+
drifts: eventSchemaDrifts,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
170
|
+
},
|
|
151
171
|
};
|
|
152
172
|
}
|
|
153
173
|
// --- Output formatters ---
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Records a spec implementation gap with severity classification and hash-chained persistence.
|
|
3
3
|
import { specStore } from '../storage/index.js';
|
|
4
4
|
import { appendGapEntry } from '../storage/gaps-log.js';
|
|
5
|
+
import { formatKeyValue } from './output-formatter.js';
|
|
5
6
|
const SPEC_ID_RE = /^SPEC-\d{3,4}$/;
|
|
6
7
|
const VALID_SEVERITIES = ['low', 'medium', 'high'];
|
|
7
8
|
/**
|
|
@@ -20,7 +21,7 @@ export async function handleFlagSpecGap(input) {
|
|
|
20
21
|
content: [
|
|
21
22
|
{
|
|
22
23
|
type: 'text',
|
|
23
|
-
text:
|
|
24
|
+
text: formatKeyValue({
|
|
24
25
|
error: 'invalid_input',
|
|
25
26
|
code: 422,
|
|
26
27
|
field: 'severity',
|
|
@@ -29,6 +30,13 @@ export async function handleFlagSpecGap(input) {
|
|
|
29
30
|
},
|
|
30
31
|
],
|
|
31
32
|
isError: true,
|
|
33
|
+
structuredContent: {
|
|
34
|
+
error: 'invalid_input',
|
|
35
|
+
code: 422,
|
|
36
|
+
field: 'severity',
|
|
37
|
+
validSeverities: VALID_SEVERITIES,
|
|
38
|
+
fixHint: `severity must be one of: ${VALID_SEVERITIES.join(', ')}`,
|
|
39
|
+
},
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
// ── Validate affectedSpecs format ─────────────────────────────────────────
|
|
@@ -38,14 +46,22 @@ export async function handleFlagSpecGap(input) {
|
|
|
38
46
|
content: [
|
|
39
47
|
{
|
|
40
48
|
type: 'text',
|
|
41
|
-
text:
|
|
49
|
+
text: formatKeyValue({
|
|
42
50
|
error: 'invalid_affected_spec',
|
|
43
51
|
code: 422,
|
|
52
|
+
specId: id,
|
|
44
53
|
fixHint: `${id} does not match the required format SPEC-NNN or SPEC-NNNN`,
|
|
45
54
|
}),
|
|
46
55
|
},
|
|
47
56
|
],
|
|
48
57
|
isError: true,
|
|
58
|
+
structuredContent: {
|
|
59
|
+
error: 'invalid_affected_spec',
|
|
60
|
+
code: 422,
|
|
61
|
+
specId: id,
|
|
62
|
+
expectedFormat: 'SPEC-NNN or SPEC-NNNN',
|
|
63
|
+
fixHint: `${id} does not match the required format SPEC-NNN or SPEC-NNNN`,
|
|
64
|
+
},
|
|
49
65
|
};
|
|
50
66
|
}
|
|
51
67
|
}
|
|
@@ -58,14 +74,22 @@ export async function handleFlagSpecGap(input) {
|
|
|
58
74
|
content: [
|
|
59
75
|
{
|
|
60
76
|
type: 'text',
|
|
61
|
-
text:
|
|
77
|
+
text: formatKeyValue({
|
|
62
78
|
error: 'invalid_affected_spec',
|
|
63
79
|
code: 422,
|
|
80
|
+
specId: id,
|
|
64
81
|
fixHint: `${id} not found in project ${projectId}`,
|
|
65
82
|
}),
|
|
66
83
|
},
|
|
67
84
|
],
|
|
68
85
|
isError: true,
|
|
86
|
+
structuredContent: {
|
|
87
|
+
error: 'invalid_affected_spec',
|
|
88
|
+
code: 422,
|
|
89
|
+
specId: id,
|
|
90
|
+
projectId,
|
|
91
|
+
fixHint: `${id} not found in project ${projectId}`,
|
|
92
|
+
},
|
|
69
93
|
};
|
|
70
94
|
}
|
|
71
95
|
}
|
|
@@ -81,25 +105,36 @@ export async function handleFlagSpecGap(input) {
|
|
|
81
105
|
content: [
|
|
82
106
|
{
|
|
83
107
|
type: 'text',
|
|
84
|
-
text:
|
|
108
|
+
text: formatKeyValue({
|
|
85
109
|
ok: true,
|
|
86
110
|
id: entry.id,
|
|
87
111
|
specId: entry.specId,
|
|
88
|
-
description: entry.description,
|
|
89
112
|
severity: entry.severity,
|
|
90
|
-
affectedSpecs: entry.affectedSpecs,
|
|
113
|
+
affectedSpecs: entry.affectedSpecs.length,
|
|
91
114
|
timestamp: entry.timestamp,
|
|
92
115
|
sha: entry.sha,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
affectedSpecs: !input.affectedSpecs || input.affectedSpecs.length === 0
|
|
96
|
-
? `[${specId}] (default)`
|
|
97
|
-
: undefined,
|
|
98
|
-
},
|
|
116
|
+
severityDefaulted: input.severity === undefined,
|
|
117
|
+
affectedSpecsDefaulted: !input.affectedSpecs || input.affectedSpecs.length === 0,
|
|
99
118
|
}),
|
|
100
119
|
},
|
|
101
120
|
],
|
|
102
121
|
isError: false,
|
|
122
|
+
structuredContent: {
|
|
123
|
+
ok: true,
|
|
124
|
+
id: entry.id,
|
|
125
|
+
specId: entry.specId,
|
|
126
|
+
description: entry.description,
|
|
127
|
+
severity: entry.severity,
|
|
128
|
+
affectedSpecs: entry.affectedSpecs,
|
|
129
|
+
timestamp: entry.timestamp,
|
|
130
|
+
sha: entry.sha,
|
|
131
|
+
defaults: {
|
|
132
|
+
severity: input.severity === undefined ? 'medium (default)' : undefined,
|
|
133
|
+
affectedSpecs: !input.affectedSpecs || input.affectedSpecs.length === 0
|
|
134
|
+
? `[${specId}] (default)`
|
|
135
|
+
: undefined,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
103
138
|
};
|
|
104
139
|
}
|
|
105
140
|
//# sourceMappingURL=flag-spec-gap.js.map
|
|
@@ -87,10 +87,8 @@ export async function handleGenerateSubAgent(args) {
|
|
|
87
87
|
}
|
|
88
88
|
const hasExtra = Object.keys(responseData).length > 0;
|
|
89
89
|
return {
|
|
90
|
-
content: [
|
|
91
|
-
|
|
92
|
-
...(hasExtra ? [{ type: 'text', text: JSON.stringify(responseData, null, 2) }] : []),
|
|
93
|
-
],
|
|
90
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
91
|
+
...(hasExtra ? { structuredContent: responseData } : {}),
|
|
94
92
|
};
|
|
95
93
|
}
|
|
96
94
|
//# sourceMappingURL=generate-sub-agent.js.map
|
|
@@ -156,8 +156,19 @@ export function handleUnlockState(state, sessionId, resourcePath, specId) {
|
|
|
156
156
|
// ---------------------------------------------------------------------------
|
|
157
157
|
function errResult(msg) {
|
|
158
158
|
return {
|
|
159
|
-
content: [{ type: 'text', text:
|
|
159
|
+
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
160
160
|
isError: true,
|
|
161
|
+
structuredContent: { error: msg },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function orchestrateToolResult(result) {
|
|
165
|
+
const status = result.granted === false ? 'blocked' : 'ok';
|
|
166
|
+
const details = result;
|
|
167
|
+
const resource = details.resourceId ? ` | resource: ${details.resourceId}` : '';
|
|
168
|
+
const message = result.message ? ` | ${result.message}` : '';
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: 'text', text: `${result.action}: ${status}${resource}${message}` }],
|
|
171
|
+
structuredContent: result,
|
|
161
172
|
};
|
|
162
173
|
}
|
|
163
174
|
export function toHeartbeatResult(sessionId, state, lockTtlMinutes) {
|
|
@@ -170,7 +181,7 @@ export function toHeartbeatResult(sessionId, state, lockTtlMinutes) {
|
|
|
170
181
|
}
|
|
171
182
|
return {
|
|
172
183
|
mutated: out.state,
|
|
173
|
-
toolResult:
|
|
184
|
+
toolResult: orchestrateToolResult(out.result),
|
|
174
185
|
};
|
|
175
186
|
}
|
|
176
187
|
export function toLockResult(sessionId, state, resourcePath, specId, lockTtlMinutes, fileOwnership) {
|
|
@@ -183,7 +194,7 @@ export function toLockResult(sessionId, state, resourcePath, specId, lockTtlMinu
|
|
|
183
194
|
}
|
|
184
195
|
return {
|
|
185
196
|
mutated: out.state,
|
|
186
|
-
toolResult:
|
|
197
|
+
toolResult: orchestrateToolResult(out.result),
|
|
187
198
|
};
|
|
188
199
|
}
|
|
189
200
|
export function toUnlockResult(sessionId, state, resourcePath, specId) {
|
|
@@ -196,7 +207,7 @@ export function toUnlockResult(sessionId, state, resourcePath, specId) {
|
|
|
196
207
|
}
|
|
197
208
|
return {
|
|
198
209
|
mutated: out.state,
|
|
199
|
-
toolResult:
|
|
210
|
+
toolResult: orchestrateToolResult(out.result),
|
|
200
211
|
};
|
|
201
212
|
}
|
|
202
213
|
//# sourceMappingURL=orchestrate-locking.js.map
|
|
@@ -278,15 +278,6 @@ export async function handleReverseEngineer(args) {
|
|
|
278
278
|
type: 'text',
|
|
279
279
|
text: ti('reverseEngineer.completed', { count: String(ca.limit) }),
|
|
280
280
|
},
|
|
281
|
-
{
|
|
282
|
-
type: 'text',
|
|
283
|
-
text: JSON.stringify({
|
|
284
|
-
specId,
|
|
285
|
-
title: spec.title,
|
|
286
|
-
status: 'draft',
|
|
287
|
-
analysis,
|
|
288
|
-
}, null, 2),
|
|
289
|
-
},
|
|
290
281
|
];
|
|
291
282
|
// AC-10: Add human-readable summary for deep-v2
|
|
292
283
|
if (deepV2Summary) {
|
|
@@ -295,6 +286,14 @@ export async function handleReverseEngineer(args) {
|
|
|
295
286
|
text: deepV2Summary,
|
|
296
287
|
});
|
|
297
288
|
}
|
|
298
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
content,
|
|
291
|
+
structuredContent: {
|
|
292
|
+
specId,
|
|
293
|
+
title: spec.title,
|
|
294
|
+
status: 'draft',
|
|
295
|
+
analysis,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
299
298
|
}
|
|
300
299
|
//# sourceMappingURL=handler.js.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { runRollback } from '../engine/release/rollback-runner.js';
|
|
3
3
|
import { writePostmortem } from '../engine/release/postmortem-generator.js';
|
|
4
4
|
import { appendTransitionEvent } from '../storage/transition-log.js';
|
|
5
|
+
import { formatKeyValue } from './output-formatter.js';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Age guard (30 days)
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -93,7 +94,7 @@ export async function handleRollbackRelease(input) {
|
|
|
93
94
|
content: [
|
|
94
95
|
{
|
|
95
96
|
type: 'text',
|
|
96
|
-
text:
|
|
97
|
+
text: formatKeyValue({
|
|
97
98
|
error: 'rollback_age_exceeded',
|
|
98
99
|
code: 422,
|
|
99
100
|
fixHint: `Versions older than ${MAX_AGE_DAYS} days require manual operator approval. Re-run with allowAged: true.`,
|
|
@@ -102,6 +103,13 @@ export async function handleRollbackRelease(input) {
|
|
|
102
103
|
},
|
|
103
104
|
],
|
|
104
105
|
isError: true,
|
|
106
|
+
structuredContent: {
|
|
107
|
+
error: 'rollback_age_exceeded',
|
|
108
|
+
code: 422,
|
|
109
|
+
fixHint: `Versions older than ${MAX_AGE_DAYS} days require manual operator approval. Re-run with allowAged: true.`,
|
|
110
|
+
ageDays: Math.round(ageDays),
|
|
111
|
+
maxAgeDays: MAX_AGE_DAYS,
|
|
112
|
+
},
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
}
|
|
@@ -178,10 +186,20 @@ export async function handleRollbackRelease(input) {
|
|
|
178
186
|
content: [
|
|
179
187
|
{
|
|
180
188
|
type: 'text',
|
|
181
|
-
text:
|
|
189
|
+
text: formatKeyValue({
|
|
190
|
+
ok: output.ok,
|
|
191
|
+
steps: output.steps.length,
|
|
192
|
+
failedSteps: output.steps.filter((step) => step.status === 'failed').length,
|
|
193
|
+
skippedSteps: output.steps.filter((step) => step.status === 'skipped').length,
|
|
194
|
+
postmortemPath: output.postmortemPath,
|
|
195
|
+
smokeTestOk: output.smokeTest?.ok,
|
|
196
|
+
hints: additionalHints.length,
|
|
197
|
+
firstHint: additionalHints[0],
|
|
198
|
+
}),
|
|
182
199
|
},
|
|
183
200
|
],
|
|
184
201
|
isError: !output.ok,
|
|
202
|
+
structuredContent: { ...output, hints: additionalHints },
|
|
185
203
|
};
|
|
186
204
|
}
|
|
187
205
|
//# sourceMappingURL=rollback-release.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// tools/spec-diff-handler.ts — Handlers for diff_spec_versions and spec_history (SPEC-284)
|
|
2
|
-
import { compactResult } from './output-formatter.js';
|
|
2
|
+
import { compactJson, compactResult } from './output-formatter.js';
|
|
3
3
|
import { projectDataDir } from '../storage/index.js';
|
|
4
4
|
import { computeSpecVersionDiff, formatDiffAsText, listSpecVersionHistory, } from '../engine/spec-differ.js';
|
|
5
5
|
/** Handler for the diff_spec_versions tool. */
|
|
@@ -21,7 +21,7 @@ export async function handleDiffSpecVersions(args) {
|
|
|
21
21
|
content: [
|
|
22
22
|
{
|
|
23
23
|
type: 'text',
|
|
24
|
-
text:
|
|
24
|
+
text: compactJson({
|
|
25
25
|
specId: diff.specId,
|
|
26
26
|
fromTag: diff.fromTag,
|
|
27
27
|
toTag: diff.toTag,
|
|
@@ -30,7 +30,7 @@ export async function handleDiffSpecVersions(args) {
|
|
|
30
30
|
linesRemoved: diff.removed.length,
|
|
31
31
|
criteriaChanges: diff.criteriaChanges,
|
|
32
32
|
sectionChanges: diff.sectionChanges,
|
|
33
|
-
}
|
|
33
|
+
}),
|
|
34
34
|
},
|
|
35
35
|
],
|
|
36
36
|
};
|
|
@@ -80,10 +80,8 @@ export async function handleSuggestMcpServer(args) {
|
|
|
80
80
|
`Estimated effort: ${hours}h`,
|
|
81
81
|
].join('\n');
|
|
82
82
|
return {
|
|
83
|
-
content: [
|
|
84
|
-
|
|
85
|
-
{ type: 'text', text: JSON.stringify(recommendation, null, 2) },
|
|
86
|
-
],
|
|
83
|
+
content: [{ type: 'text', text: summary }],
|
|
84
|
+
structuredContent: recommendation,
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
//# sourceMappingURL=suggest-mcp-server.js.map
|
|
@@ -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
|
*
|
|
@@ -163,29 +164,36 @@ export async function checkDodGate(spec, specId, projectId, projectPath, _force)
|
|
|
163
164
|
}).catch((err) => {
|
|
164
165
|
console.error('[planu] DoD feedback event failed:', err);
|
|
165
166
|
});
|
|
166
|
-
const failedItems = requiredItems
|
|
167
|
-
.filter((i) => i.status === 'failed')
|
|
168
|
-
.map((i) => ({ id: i.id, description: i.description, category: i.category }));
|
|
169
167
|
return {
|
|
170
168
|
content: [
|
|
171
169
|
{
|
|
172
170
|
type: 'text',
|
|
173
|
-
text:
|
|
174
|
-
|
|
175
|
-
message: humanMessage,
|
|
171
|
+
text: [
|
|
172
|
+
humanMessage,
|
|
176
173
|
expertSummary,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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'),
|
|
184
182
|
},
|
|
185
183
|
],
|
|
186
184
|
isError: true,
|
|
187
185
|
structuredContent: {
|
|
188
|
-
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',
|
|
189
197
|
code: 422,
|
|
190
198
|
context: {
|
|
191
199
|
specId,
|
|
@@ -408,13 +416,14 @@ function validationReportGateError(args) {
|
|
|
408
416
|
content: [
|
|
409
417
|
{
|
|
410
418
|
type: 'text',
|
|
411
|
-
text:
|
|
419
|
+
text: formatKeyValue({
|
|
412
420
|
error: args.error,
|
|
413
421
|
message: args.message,
|
|
414
422
|
artifactPath,
|
|
415
|
-
|
|
423
|
+
failedGates: args.gates.filter((gate) => !gate.passed).length,
|
|
424
|
+
totalGates: args.gates.length,
|
|
416
425
|
fixHint: args.fixHint,
|
|
417
|
-
},
|
|
426
|
+
}, 'Validation report gate failed'),
|
|
418
427
|
},
|
|
419
428
|
],
|
|
420
429
|
isError: true,
|
|
@@ -432,13 +441,14 @@ function specReviewGateError(args) {
|
|
|
432
441
|
content: [
|
|
433
442
|
{
|
|
434
443
|
type: 'text',
|
|
435
|
-
text:
|
|
444
|
+
text: formatKeyValue({
|
|
436
445
|
error: args.error,
|
|
437
446
|
message: args.message,
|
|
438
447
|
artifactPath,
|
|
439
|
-
blockers: args.blockers,
|
|
448
|
+
blockers: args.blockers.length,
|
|
449
|
+
firstBlocker: args.blockers[0],
|
|
440
450
|
fixHint: args.fixHint,
|
|
441
|
-
},
|
|
451
|
+
}, 'Spec review gate failed'),
|
|
442
452
|
},
|
|
443
453
|
],
|
|
444
454
|
isError: true,
|
|
@@ -556,17 +566,22 @@ export async function checkApprovedFormatGate(spec, newStatus, forceApprove = fa
|
|
|
556
566
|
content: [
|
|
557
567
|
{
|
|
558
568
|
type: 'text',
|
|
559
|
-
text:
|
|
569
|
+
text: formatKeyValue({
|
|
560
570
|
error: 'SPEC_FORMAT_INVALID',
|
|
561
571
|
message: `Spec format validation failed with ${String(result.errors.length)} error(s) — fix before approving, or pass forceApprove: true to bypass with warnings.`,
|
|
562
|
-
errors: result.errors,
|
|
563
|
-
warnings: result.warnings,
|
|
564
|
-
|
|
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'),
|
|
565
577
|
},
|
|
566
578
|
],
|
|
567
579
|
isError: true,
|
|
568
580
|
structuredContent: {
|
|
569
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,
|
|
570
585
|
code: 422,
|
|
571
586
|
context: {
|
|
572
587
|
specId: spec.id,
|