@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
  3. package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
  4. package/dist/engine/evidence-index/done-drift.js +4 -1
  5. package/dist/engine/local-first/tool-classification.d.ts +7 -0
  6. package/dist/engine/local-first/tool-classification.js +313 -0
  7. package/dist/engine/qa-gate.js +13 -1
  8. package/dist/engine/scope-boundaries/scope-validator.js +14 -2
  9. package/dist/engine/validator/validation-report-writer.js +3 -4
  10. package/dist/tools/audit.js +1 -1
  11. package/dist/tools/challenge-spec.js +52 -2
  12. package/dist/tools/check-readiness.js +15 -8
  13. package/dist/tools/data-governance/audit-handler.js +21 -6
  14. package/dist/tools/data-governance/detect-handler.js +32 -15
  15. package/dist/tools/define-ui-contract.js +1 -4
  16. package/dist/tools/design-schema.js +1 -4
  17. package/dist/tools/detect-drift.js +48 -28
  18. package/dist/tools/flag-spec-gap.js +47 -12
  19. package/dist/tools/generate-sub-agent.js +2 -4
  20. package/dist/tools/list-specs.js +15 -59
  21. package/dist/tools/orchestrate-locking.js +15 -4
  22. package/dist/tools/output-compressor.d.ts +14 -1
  23. package/dist/tools/output-compressor.js +121 -2
  24. package/dist/tools/package-handoff.js +136 -12
  25. package/dist/tools/reverse-engineer/handler.js +9 -10
  26. package/dist/tools/rollback-release.js +20 -2
  27. package/dist/tools/spec-diff-handler.js +3 -3
  28. package/dist/tools/status-handler.js +6 -1
  29. package/dist/tools/suggest-mcp-server.js +2 -4
  30. package/dist/tools/token-recording.d.ts +2 -1
  31. package/dist/tools/token-recording.js +21 -2
  32. package/dist/tools/update-status/dod-gates.d.ts +2 -2
  33. package/dist/tools/update-status/dod-gates.js +47 -52
  34. package/dist/tools/update-status/evidence-gate.js +6 -4
  35. package/dist/tools/update-status/index.js +10 -3
  36. package/dist/tools/update-status/qa-gate.d.ts +5 -0
  37. package/dist/tools/update-status/qa-gate.js +50 -0
  38. package/dist/tools/update-status/response-builder.js +96 -2
  39. package/dist/tools/update-status/transition-guard.js +33 -11
  40. package/dist/tools/update-status-convention-gate.js +22 -6
  41. package/dist/tools/validate-team-results.js +23 -1
  42. package/dist/tools/validate.js +137 -16
  43. package/dist/transports/http-transport.js +13 -0
  44. package/dist/transports/transport-factory.js +13 -0
  45. package/dist/types/qa-gate.d.ts +3 -0
  46. package/dist/types/tool-classification.d.ts +8 -0
  47. package/dist/types/tool-classification.js +2 -0
  48. package/package.json +10 -10
  49. package/planu-native.json +1 -1
  50. package/planu-plugin.json +1 -1
@@ -3,6 +3,7 @@ import { specStore, knowledgeStore } from '../../storage/index.js';
3
3
  import { ti } from '../../i18n/index.js';
4
4
  import { detectPIIInSpec, detectLegalFramework } from '../../engine/pii-detector.js';
5
5
  import { readFile } from 'node:fs/promises';
6
+ import { formatKeyValue } from '../output-formatter.js';
6
7
  // ---------------------------------------------------------------------------
7
8
  // privacy-notice subcommand
8
9
  // ---------------------------------------------------------------------------
@@ -40,14 +41,21 @@ export async function handlePrivacyNotice(projectId) {
40
41
  },
41
42
  {
42
43
  type: 'text',
43
- text: JSON.stringify({
44
+ text: formatKeyValue({
44
45
  subcommand: 'privacy-notice',
45
46
  specsWithPII: privacyItems.length,
46
47
  totalSpecs: specs.length,
47
- noticeText: notice,
48
- }, null, 2),
48
+ noticeChars: notice.length,
49
+ }),
49
50
  },
50
51
  ],
52
+ structuredContent: {
53
+ subcommand: 'privacy-notice',
54
+ specsWithPII: privacyItems.length,
55
+ totalSpecs: specs.length,
56
+ noticeText: notice,
57
+ items: privacyItems,
58
+ },
51
59
  };
52
60
  }
53
61
  // ---------------------------------------------------------------------------
@@ -105,16 +113,23 @@ export async function handleAudit(projectId) {
105
113
  },
106
114
  {
107
115
  type: 'text',
108
- text: JSON.stringify({
116
+ text: formatKeyValue({
109
117
  subcommand: 'audit',
110
118
  totalSpecs: specs.length,
111
119
  specsWithPII: auditItems.filter((i) => i.hasPII).length,
112
120
  nonCompliantSpecs: nonCompliant,
113
121
  framework,
114
- items: auditItems,
115
- }, null, 2),
122
+ }),
116
123
  },
117
124
  ],
125
+ structuredContent: {
126
+ subcommand: 'audit',
127
+ totalSpecs: specs.length,
128
+ specsWithPII: auditItems.filter((i) => i.hasPII).length,
129
+ nonCompliantSpecs: nonCompliant,
130
+ framework,
131
+ items: auditItems,
132
+ },
118
133
  };
119
134
  }
120
135
  // ---------------------------------------------------------------------------
@@ -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
@@ -4,16 +4,15 @@ import { buildListSpecsSummary } from '../engine/human-summary.js';
4
4
  import { dirname } from 'node:path';
5
5
  import { checkBundledVersionGap } from '../engine/version-detector/bundled-version-checker.js';
6
6
  import { readLastModifiedTimestamp, formatRelativeDate } from '../engine/spec-changelog/index.js';
7
- import { discoverAndFlattenSpecs, filterUnprefixedSpecs, importFilesystemSpecs, migrateSpecFolderNames, reconcileSpecPaths, scanForAmbiguousCriteria, } from '../engine/spec-migrator.js';
7
+ import { scanForAmbiguousCriteria } from '../engine/spec-migrator.js';
8
8
  import { detectDrift, formatDriftMessage } from '../engine/spec-migrator/drift-detector.js';
9
9
  import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
10
- import { withAudit } from '../engine/autopilot/audit-logger.js';
11
10
  import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
12
- import { refreshProjectStatus } from '../engine/autopilot/state-updater.js';
13
11
  import { resolveProjectIdOrAutoDetect } from './resolve-project-id.js';
14
12
  import { isNativeActive, fastScanSpecs } from '../engine/core-bridge.js';
15
13
  /** Track which projects have been auto-discovered this session (once per project). */
16
14
  const discoveredProjects = new Set();
15
+ const MAX_STRUCTURED_SPECS = 50;
17
16
  /** Build markdown table rows for summary mode */
18
17
  function buildSummaryTable(specs) {
19
18
  const header = '| ID | Title | Status | Type | Tags |';
@@ -21,48 +20,11 @@ function buildSummaryTable(specs) {
21
20
  const rows = specs.map((s) => `| ${s.id} | ${s.title} | ${s.status} | ${s.type} | ${s.tags.join(', ')} |`);
22
21
  return [header, divider, ...rows].join('\n');
23
22
  }
24
- async function autoDiscoverProject(projectId, projectPath, collector) {
23
+ async function collectReadOnlyProjectSignals(projectId, projectPath, collector) {
25
24
  if (discoveredProjects.has(projectId)) {
26
25
  return;
27
26
  }
28
27
  discoveredProjects.add(projectId);
29
- try {
30
- await withAudit(projectPath, 'list_specs', 'discoverAndFlattenSpecs', () => discoverAndFlattenSpecs(projectPath));
31
- }
32
- catch {
33
- /* best-effort — don't fail list_specs */
34
- }
35
- try {
36
- await withAudit(projectPath, 'list_specs', 'importFilesystemSpecs', () => importFilesystemSpecs(projectPath, {
37
- listSpecs: specStore.listSpecs,
38
- createSpec: specStore.createSpec,
39
- }, projectId), (imported) => ({ imported: Array.isArray(imported) ? imported.length : 0 }));
40
- }
41
- catch {
42
- /* best-effort */
43
- }
44
- try {
45
- await withAudit(projectPath, 'list_specs', 'reconcileSpecPaths', () => reconcileSpecPaths(projectPath, {
46
- listSpecs: specStore.listSpecs,
47
- updateSpec: specStore.updateSpec,
48
- }, projectId));
49
- }
50
- catch {
51
- /* best-effort */
52
- }
53
- try {
54
- const allStoredSpecs = await specStore.listSpecs(projectId);
55
- const unprefixed = filterUnprefixedSpecs(allStoredSpecs);
56
- if (unprefixed.length > 0) {
57
- await withAudit(projectPath, 'list_specs', 'migrateSpecFolderNames', () => migrateSpecFolderNames(projectPath, unprefixed, {
58
- listSpecs: specStore.listSpecs,
59
- updateSpec: specStore.updateSpec,
60
- }), (migrated) => ({ count: Array.isArray(migrated) ? migrated.length : 0 }));
61
- }
62
- }
63
- catch {
64
- /* best-effort */
65
- }
66
28
  // SPEC-1017: list_specs enforces the strict planu/ policy in check mode.
67
29
  // Mutating cleanup is handled by init/update/validate/housekeeping; list_specs
68
30
  // stays read-mostly and surfaces exact offenders if any remain.
@@ -88,21 +50,6 @@ async function autoDiscoverProject(projectId, projectPath, collector) {
88
50
  catch {
89
51
  /* best-effort — never block list_specs */
90
52
  }
91
- try {
92
- const { configureGitignoreForPlanu } = await import('./init-project/git-setup.js');
93
- await withAudit(projectPath, 'list_specs', 'configureGitignoreForPlanu', () => configureGitignoreForPlanu(projectPath));
94
- collector.pushOk('gitignore', 'Updated .gitignore with planu/ rules');
95
- }
96
- catch {
97
- /* best-effort */
98
- }
99
- // SPEC-493: Silently repair status.json totalSpecs count from actual store data
100
- try {
101
- await withAudit(projectPath, 'list_specs', 'refreshProjectStatus', () => refreshProjectStatus(projectPath, projectId));
102
- }
103
- catch {
104
- /* best-effort */
105
- }
106
53
  }
107
54
  export async function handleListSpecs(params) {
108
55
  // SPEC-584: list_specs is read-mostly and its migration-issue questions are opt-in.
@@ -152,9 +99,10 @@ export async function handleListSpecs(params) {
152
99
  if (isNativeActive() && knowledge.projectPath) {
153
100
  nativeBriefs = fastScanSpecs(knowledge.projectPath);
154
101
  }
155
- // Auto-discover, flatten, rename, and fix specs (once per session per project)
102
+ // Read-only project signals only. Mutating discovery/migration/repair belongs
103
+ // to init, validate, housekeeping, or explicit migration tools.
156
104
  if (knowledge.projectPath) {
157
- await autoDiscoverProject(projectId, knowledge.projectPath, collector);
105
+ await collectReadOnlyProjectSignals(projectId, knowledge.projectPath, collector);
158
106
  }
159
107
  // Get all specs
160
108
  let specs = await specStore.listSpecs(projectId);
@@ -247,6 +195,8 @@ export async function handleListSpecs(params) {
247
195
  updatedAt: s.updatedAt,
248
196
  lastModified: lastModifiedMap.get(s.id) ?? '\u2014',
249
197
  }));
198
+ const structuredSpecs = specsData.slice(0, MAX_STRUCTURED_SPECS);
199
+ const specsOmitted = Math.max(0, specsData.length - structuredSpecs.length);
250
200
  // Build text content: markdown table in summary mode, JSON in full mode
251
201
  let textContent;
252
202
  if (detail === 'summary') {
@@ -262,7 +212,13 @@ export async function handleListSpecs(params) {
262
212
  },
263
213
  count: specs.length,
264
214
  totalCount: allSpecs.length,
265
- specs: specsData,
215
+ specs: structuredSpecs,
216
+ pagination: {
217
+ limit: MAX_STRUCTURED_SPECS,
218
+ returned: structuredSpecs.length,
219
+ total: specsData.length,
220
+ omitted: specsOmitted,
221
+ },
266
222
  summary: {
267
223
  byStatus: statusCounts,
268
224
  byType: typeCounts,
@@ -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
@@ -1,8 +1,21 @@
1
1
  import type { ToolResult } from '../types/index.js';
2
+ export interface StructuredContentCompactionOptions {
3
+ maxArrayItems?: number;
4
+ maxStringChars?: number;
5
+ maxObjectKeys?: number;
6
+ maxDepth?: number;
7
+ }
8
+ /**
9
+ * Compacts structuredContent without changing its broad contract shape.
10
+ * Arrays remain arrays, objects remain objects, and compaction metadata is added
11
+ * at top level when a successful response would otherwise return a large payload.
12
+ */
13
+ export declare function compactStructuredContent(structuredContent: Record<string, unknown>, options?: StructuredContentCompactionOptions): Record<string, unknown>;
2
14
  /**
3
15
  * Post-processes a ToolResult to compress verbose content blocks.
4
16
  * Applied to ALL tool outputs via safeWithTelemetry.
5
- * Preserves structuredContent untouched only compresses text blocks.
17
+ * Also bounds structuredContent so token recording reflects the actual visible
18
+ * payload instead of unbounded internal data.
6
19
  */
7
20
  export declare function compressToolOutput(result: ToolResult): ToolResult;
8
21
  //# sourceMappingURL=output-compressor.d.ts.map