@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.
@@ -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: JSON.stringify({
82
+ text: formatKeyValue({
82
83
  subcommand: 'detect',
83
- detectionResult: result,
84
+ hasPII: result.hasPII,
85
+ piiFields: result.fields.length,
86
+ sensitiveSpecialCount: result.sensitiveSpecialCount,
84
87
  framework,
85
- retentionPolicy,
86
- requirement,
87
- }, null, 2),
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: JSON.stringify({
128
+ text: formatKeyValue({
119
129
  subcommand: 'retention-policy',
120
130
  modelName: effectiveModel,
121
131
  framework,
122
- retentionPolicy: defaultRetentionPolicy,
123
- auditFields: [
124
- 'created_at',
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
- ? [{ type: 'text', text: JSON.stringify({ planProgress }, null, 2) }]
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: JSON.stringify({
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: JSON.stringify({
121
- privacyDrift: {
122
- count: privacyDrifts.length,
123
- criticalCount: privacyDrifts.filter((d) => d.severity === 'critical').length,
124
- drifts: privacyDrifts,
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: JSON.stringify({
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: JSON.stringify({
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: JSON.stringify({
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: JSON.stringify({
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: JSON.stringify({
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
- defaults: {
94
- severity: input.severity === undefined ? 'medium (default)' : undefined,
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
- { type: 'text', text: lines.join('\n') },
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: JSON.stringify({ error: msg }, null, 2) }],
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: { content: [{ type: 'text', text: JSON.stringify(out.result, null, 2) }] },
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: { content: [{ type: 'text', text: JSON.stringify(out.result, null, 2) }] },
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: { content: [{ type: 'text', text: JSON.stringify(out.result, null, 2) }] },
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 { content };
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: JSON.stringify({
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: JSON.stringify({ ...output, hints: additionalHints }),
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: JSON.stringify({
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
- }, null, 2),
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
- { type: 'text', text: summary },
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: JSON.stringify({
174
- error: 'DoD gate failed',
175
- message: humanMessage,
171
+ text: [
172
+ humanMessage,
176
173
  expertSummary,
177
- blockingItems,
178
- failedItems,
179
- warningItems,
180
- fixHints,
181
- gateResults: dod.gateResults ?? null,
182
- hint: '💡 Alternatively: use force:true to bypass all gates',
183
- }, null, 2),
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: 'dod_gate_failed',
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: JSON.stringify({
419
+ text: formatKeyValue({
412
420
  error: args.error,
413
421
  message: args.message,
414
422
  artifactPath,
415
- gates: args.gates,
423
+ failedGates: args.gates.filter((gate) => !gate.passed).length,
424
+ totalGates: args.gates.length,
416
425
  fixHint: args.fixHint,
417
- }, null, 2),
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: JSON.stringify({
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
- }, null, 2),
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: JSON.stringify({
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
- }, null, 2),
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,