@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
|
@@ -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:
|
|
44
|
+
text: formatKeyValue({
|
|
44
45
|
subcommand: 'privacy-notice',
|
|
45
46
|
specsWithPII: privacyItems.length,
|
|
46
47
|
totalSpecs: specs.length,
|
|
47
|
-
|
|
48
|
-
}
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
package/dist/tools/list-specs.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
@@ -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
|
-
*
|
|
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
|