@planu/cli 4.8.0 → 4.10.0
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 +22 -0
- package/dist/config/license-plans.json +5 -0
- package/dist/config/project-knowledge-graph.json +70 -2
- package/dist/config/registries/hosts/codex.json +3 -4
- package/dist/engine/evidence-index/index-builder.js +38 -12
- package/dist/engine/project-graph/builder.js +9 -5
- package/dist/engine/project-graph/cache.js +4 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.d.ts +3 -0
- package/dist/engine/project-graph/extractors/decision-store-extractor.js +57 -0
- package/dist/engine/structural-memory/architecture-snapshot.d.ts +3 -0
- package/dist/engine/structural-memory/architecture-snapshot.js +132 -0
- package/dist/engine/structural-memory/artifact-policy.d.ts +5 -0
- package/dist/engine/structural-memory/artifact-policy.js +17 -0
- package/dist/engine/structural-memory/change-impact.d.ts +3 -0
- package/dist/engine/structural-memory/change-impact.js +159 -0
- package/dist/engine/structural-memory/contracts.d.ts +2 -0
- package/dist/engine/structural-memory/contracts.js +2 -0
- package/dist/engine/structural-memory/helpers.d.ts +28 -0
- package/dist/engine/structural-memory/helpers.js +122 -0
- package/dist/engine/structural-memory/index.d.ts +7 -0
- package/dist/engine/structural-memory/index.js +6 -0
- package/dist/engine/structural-memory/query.d.ts +3 -0
- package/dist/engine/structural-memory/query.js +67 -0
- package/dist/engine/structural-memory/status.d.ts +3 -0
- package/dist/engine/structural-memory/status.js +30 -0
- package/dist/storage/feedback-remote.d.ts +5 -1
- package/dist/storage/feedback-remote.js +75 -16
- package/dist/storage/feedback-store.d.ts +3 -1
- package/dist/storage/feedback-store.js +107 -10
- package/dist/tools/feedback-handler.d.ts +3 -1
- package/dist/tools/feedback-handler.js +47 -1
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +5 -0
- package/dist/tools/status-handler.js +11 -11
- package/dist/tools/tool-registry/core-tools.js +97 -0
- package/dist/tools/tool-registry.js +3 -0
- package/dist/tools/validate-assurance.d.ts +6 -0
- package/dist/tools/validate-assurance.js +55 -0
- package/dist/tools/validate-graph-coverage.d.ts +8 -0
- package/dist/tools/validate-graph-coverage.js +30 -0
- package/dist/tools/validate-helpers.d.ts +9 -0
- package/dist/tools/validate-helpers.js +47 -0
- package/dist/tools/validate-lint.d.ts +3 -0
- package/dist/tools/validate-lint.js +67 -0
- package/dist/tools/validate-minimality.d.ts +16 -0
- package/dist/tools/validate-minimality.js +22 -0
- package/dist/tools/validate-runtime.d.ts +14 -0
- package/dist/tools/validate-runtime.js +85 -0
- package/dist/tools/validate.js +5 -222
- package/dist/types/evidence-index.d.ts +1 -0
- package/dist/types/feedback.d.ts +37 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/project-knowledge-graph.d.ts +10 -0
- package/dist/types/structural-memory.d.ts +138 -0
- package/dist/types/structural-memory.js +2 -0
- package/package.json +20 -20
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { withFileLock } from './file-mutex.js';
|
|
5
|
-
import {
|
|
5
|
+
import { deliverFeedbackRemote } from './feedback-remote.js';
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Path helpers
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -37,6 +37,54 @@ function writeLedger(filePath, entries) {
|
|
|
37
37
|
ensureDir(dirname(filePath));
|
|
38
38
|
writeFileSync(filePath, JSON.stringify(entries, null, 2), 'utf-8');
|
|
39
39
|
}
|
|
40
|
+
function getRemoteStatus(entry) {
|
|
41
|
+
return entry.remoteDelivery?.status ?? 'pending';
|
|
42
|
+
}
|
|
43
|
+
function toRemotePayload(entry) {
|
|
44
|
+
return {
|
|
45
|
+
id: entry.id,
|
|
46
|
+
type: entry.type,
|
|
47
|
+
title: entry.title,
|
|
48
|
+
description: entry.description,
|
|
49
|
+
planuVersion: entry.planuVersion,
|
|
50
|
+
stack: entry.context?.stack,
|
|
51
|
+
tags: entry.tags,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function toRemoteDelivery(entry, result) {
|
|
55
|
+
const now = new Date().toISOString();
|
|
56
|
+
const previousAttempts = entry.remoteDelivery?.attemptCount ?? 0;
|
|
57
|
+
return {
|
|
58
|
+
status: result.status,
|
|
59
|
+
attemptCount: previousAttempts + 1,
|
|
60
|
+
lastAttemptAt: now,
|
|
61
|
+
...(result.ok ? { lastSyncedAt: now } : {}),
|
|
62
|
+
...(result.error !== undefined ? { lastError: result.error.slice(0, 300) } : {}),
|
|
63
|
+
...(result.endpoint !== undefined ? { endpoint: result.endpoint } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function updateRemoteDelivery(dataDir, id, result) {
|
|
67
|
+
const filePath = getFeedbackPath(dataDir);
|
|
68
|
+
return withFileLock(filePath, () => {
|
|
69
|
+
const entries = readLedger(filePath);
|
|
70
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
71
|
+
const existing = entries[idx];
|
|
72
|
+
if (idx === -1 || existing === undefined) {
|
|
73
|
+
return Promise.resolve(null);
|
|
74
|
+
}
|
|
75
|
+
const updated = {
|
|
76
|
+
...existing,
|
|
77
|
+
remoteDelivery: toRemoteDelivery(existing, result),
|
|
78
|
+
};
|
|
79
|
+
entries[idx] = updated;
|
|
80
|
+
writeLedger(filePath, entries);
|
|
81
|
+
return Promise.resolve(updated);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function deliverAndPersistRemote(dataDir, entry) {
|
|
85
|
+
const result = await deliverFeedbackRemote(toRemotePayload(entry));
|
|
86
|
+
return (await updateRemoteDelivery(dataDir, entry.id, result)) ?? entry;
|
|
87
|
+
}
|
|
40
88
|
// ---------------------------------------------------------------------------
|
|
41
89
|
// ID generation
|
|
42
90
|
// ---------------------------------------------------------------------------
|
|
@@ -144,18 +192,12 @@ export async function submitFeedback(dataDir, entry) {
|
|
|
144
192
|
id,
|
|
145
193
|
createdAt: new Date().toISOString(),
|
|
146
194
|
planuVersion: version,
|
|
195
|
+
remoteDelivery: { status: 'pending', attemptCount: 0 },
|
|
147
196
|
};
|
|
148
197
|
entries.push(newEntry);
|
|
149
198
|
writeLedger(filePath, entries);
|
|
150
|
-
void
|
|
151
|
-
|
|
152
|
-
title: newEntry.title,
|
|
153
|
-
description: newEntry.description,
|
|
154
|
-
planuVersion: newEntry.planuVersion,
|
|
155
|
-
stack: newEntry.context?.stack,
|
|
156
|
-
tags: newEntry.tags,
|
|
157
|
-
}).catch((err) => {
|
|
158
|
-
console.error('[planu] sendFeedbackRemote failed:', err);
|
|
199
|
+
void deliverAndPersistRemote(dataDir, newEntry).catch((err) => {
|
|
200
|
+
console.error('[planu] feedback remote delivery failed:', err);
|
|
159
201
|
});
|
|
160
202
|
return Promise.resolve(newEntry);
|
|
161
203
|
});
|
|
@@ -259,6 +301,61 @@ export function getSummary(dataDir) {
|
|
|
259
301
|
topRequests,
|
|
260
302
|
};
|
|
261
303
|
}
|
|
304
|
+
export function getFeedbackRemoteHealth(dataDir) {
|
|
305
|
+
const entries = readLedger(getFeedbackPath(dataDir));
|
|
306
|
+
const byRemoteStatus = {
|
|
307
|
+
pending: 0,
|
|
308
|
+
synced: 0,
|
|
309
|
+
failed: 0,
|
|
310
|
+
skipped: 0,
|
|
311
|
+
};
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
byRemoteStatus[getRemoteStatus(entry)]++;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
byRemoteStatus,
|
|
317
|
+
retryable: byRemoteStatus.pending + byRemoteStatus.failed,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export async function syncFeedbackRemote(dataDir, input = {}) {
|
|
321
|
+
const limit = input.limit !== undefined && input.limit > 0 ? input.limit : Number.POSITIVE_INFINITY;
|
|
322
|
+
const entries = readLedger(getFeedbackPath(dataDir));
|
|
323
|
+
const summary = {
|
|
324
|
+
total: 0,
|
|
325
|
+
synced: 0,
|
|
326
|
+
failed: 0,
|
|
327
|
+
skipped: 0,
|
|
328
|
+
alreadySynced: 0,
|
|
329
|
+
processedIds: [],
|
|
330
|
+
};
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (summary.total >= limit) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
const status = getRemoteStatus(entry);
|
|
336
|
+
if (status === 'synced' && input.includeSynced !== true) {
|
|
337
|
+
summary.alreadySynced++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (!['pending', 'failed', 'skipped', 'synced'].includes(status)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
summary.total++;
|
|
344
|
+
summary.processedIds.push(entry.id);
|
|
345
|
+
const updated = await deliverAndPersistRemote(dataDir, entry);
|
|
346
|
+
const updatedStatus = getRemoteStatus(updated);
|
|
347
|
+
if (updatedStatus === 'synced') {
|
|
348
|
+
summary.synced++;
|
|
349
|
+
}
|
|
350
|
+
else if (updatedStatus === 'skipped') {
|
|
351
|
+
summary.skipped++;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
summary.failed++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return summary;
|
|
358
|
+
}
|
|
262
359
|
/**
|
|
263
360
|
* Find feedback entries with title similar to the given string.
|
|
264
361
|
* Uses Jaccard word overlap. Threshold defaults to 0.3.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { ToolResult, SubmitFeedbackInput, TriageFeedbackInput, ResolveFeedbackInput } from '../types/index.js';
|
|
1
|
+
import type { ToolResult, SubmitFeedbackInput, TriageFeedbackInput, ResolveFeedbackInput, SyncFeedbackInput, FeedbackStatusInput } from '../types/index.js';
|
|
2
2
|
export declare function handleSubmitFeedback(input: SubmitFeedbackInput): Promise<ToolResult>;
|
|
3
3
|
export declare function handleTriageFeedback(input: TriageFeedbackInput): ToolResult;
|
|
4
4
|
export declare function handleResolveFeedback(input: ResolveFeedbackInput): Promise<ToolResult>;
|
|
5
|
+
export declare function handleSyncFeedback(input: SyncFeedbackInput): Promise<ToolResult>;
|
|
6
|
+
export declare function handleFeedbackStatus(input: FeedbackStatusInput): ToolResult;
|
|
5
7
|
//# sourceMappingURL=feedback-handler.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// tools/feedback-handler.ts — Feedback Hub tool handlers (SPEC-188)
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { submitFeedback, getFeedback, resolveFeedback, findSimilar, } from '../storage/feedback-store.js';
|
|
3
|
+
import { submitFeedback, getFeedback, resolveFeedback, findSimilar, getSummary, getFeedbackRemoteHealth, syncFeedbackRemote, } from '../storage/feedback-store.js';
|
|
4
4
|
import { triageFeedback, formatTriageReport } from '../engine/feedback/triage.js';
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Default data directory
|
|
@@ -43,6 +43,7 @@ export async function handleSubmitFeedback(input) {
|
|
|
43
43
|
`| Type | ${TYPE_EMOJI[feedbackType]} |`,
|
|
44
44
|
`| Title | ${entry.title} |`,
|
|
45
45
|
`| Status | Pending |`,
|
|
46
|
+
`| Remote Delivery | ${entry.remoteDelivery?.status ?? 'pending'} |`,
|
|
46
47
|
];
|
|
47
48
|
if (topSimilar !== undefined) {
|
|
48
49
|
lines.push('');
|
|
@@ -103,4 +104,49 @@ export async function handleResolveFeedback(input) {
|
|
|
103
104
|
}
|
|
104
105
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
105
106
|
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// sync_feedback handler
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
export async function handleSyncFeedback(input) {
|
|
111
|
+
const dataDir = getDataDir(input.projectPath);
|
|
112
|
+
const summary = await syncFeedbackRemote(dataDir, input);
|
|
113
|
+
const lines = [
|
|
114
|
+
'Feedback remote sync complete.',
|
|
115
|
+
'',
|
|
116
|
+
'| Field | Value |',
|
|
117
|
+
'|-------|-------|',
|
|
118
|
+
`| Processed | ${summary.total} |`,
|
|
119
|
+
`| Synced | ${summary.synced} |`,
|
|
120
|
+
`| Failed | ${summary.failed} |`,
|
|
121
|
+
`| Skipped | ${summary.skipped} |`,
|
|
122
|
+
`| Already Synced | ${summary.alreadySynced} |`,
|
|
123
|
+
];
|
|
124
|
+
if (summary.processedIds.length > 0) {
|
|
125
|
+
lines.push(`| Processed IDs | ${summary.processedIds.join(', ')} |`);
|
|
126
|
+
}
|
|
127
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// feedback_status handler
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export function handleFeedbackStatus(input) {
|
|
133
|
+
const dataDir = getDataDir(input.projectPath);
|
|
134
|
+
const summary = getSummary(dataDir);
|
|
135
|
+
const remote = getFeedbackRemoteHealth(dataDir);
|
|
136
|
+
const lines = [
|
|
137
|
+
'Feedback status',
|
|
138
|
+
'',
|
|
139
|
+
'| Field | Value |',
|
|
140
|
+
'|-------|-------|',
|
|
141
|
+
`| Total | ${summary.total} |`,
|
|
142
|
+
`| Pending Local | ${summary.pending} |`,
|
|
143
|
+
`| Resolved Local | ${summary.resolved} |`,
|
|
144
|
+
`| Remote Pending | ${remote.byRemoteStatus.pending} |`,
|
|
145
|
+
`| Remote Synced | ${remote.byRemoteStatus.synced} |`,
|
|
146
|
+
`| Remote Failed | ${remote.byRemoteStatus.failed} |`,
|
|
147
|
+
`| Remote Skipped | ${remote.byRemoteStatus.skipped} |`,
|
|
148
|
+
`| Retryable | ${remote.retryable} |`,
|
|
149
|
+
];
|
|
150
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
151
|
+
}
|
|
106
152
|
//# sourceMappingURL=feedback-handler.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
export declare const OFFICIAL_SDD_TOOL_NAMES: readonly ["planu_status", "facilitate", "init_project", "clarify_requirements", "create_spec", "challenge_spec", "check_readiness", "update_status", "update_status_batch", "package_handoff", "validate", "reconcile_spec", "create_rule", "create_skill", "skill_search", "configure_code_graph", "code_graph_status"];
|
|
2
|
+
export declare const OFFICIAL_SDD_TOOL_NAMES: readonly ["planu_status", "facilitate", "init_project", "clarify_requirements", "create_spec", "challenge_spec", "check_readiness", "update_status", "update_status_batch", "package_handoff", "validate", "reconcile_spec", "create_rule", "create_skill", "skill_search", "configure_code_graph", "code_graph_status", "submit_feedback", "triage_feedback", "resolve_feedback", "sync_feedback", "feedback_status"];
|
|
3
3
|
export declare function registerSddTools(server: McpServer): void;
|
|
4
4
|
//# sourceMappingURL=register-sdd-tools.d.ts.map
|
|
@@ -30,6 +30,11 @@ export const OFFICIAL_SDD_TOOL_NAMES = [
|
|
|
30
30
|
'skill_search',
|
|
31
31
|
'configure_code_graph',
|
|
32
32
|
'code_graph_status',
|
|
33
|
+
'submit_feedback',
|
|
34
|
+
'triage_feedback',
|
|
35
|
+
'resolve_feedback',
|
|
36
|
+
'sync_feedback',
|
|
37
|
+
'feedback_status',
|
|
33
38
|
];
|
|
34
39
|
const OFFICIAL_SDD_TOOL_SET = new Set(OFFICIAL_SDD_TOOL_NAMES);
|
|
35
40
|
const noop = () => undefined;
|
|
@@ -27,19 +27,13 @@ const execFile = promisify(execFileCb);
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
async function getGitState(projectPath) {
|
|
29
29
|
try {
|
|
30
|
-
const [
|
|
31
|
-
execFile('git', ['
|
|
30
|
+
const [stagedResult, modifiedResult, branchResult] = await Promise.all([
|
|
31
|
+
execFile('git', ['diff', '--cached', '--name-only'], { cwd: projectPath }),
|
|
32
|
+
execFile('git', ['diff', '--name-only'], { cwd: projectPath }),
|
|
32
33
|
execFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath }),
|
|
33
34
|
]);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const ch = l[0];
|
|
37
|
-
return ch !== ' ' && ch !== '?';
|
|
38
|
-
}).length;
|
|
39
|
-
const modified = lines.filter((l) => {
|
|
40
|
-
const ch = l[1];
|
|
41
|
-
return ch !== ' ' && ch !== '?';
|
|
42
|
-
}).length;
|
|
35
|
+
const staged = countGitPathLines(stagedResult.stdout);
|
|
36
|
+
const modified = countGitPathLines(modifiedResult.stdout);
|
|
43
37
|
const branch = branchResult.stdout.trim();
|
|
44
38
|
return { staged, modified, branch };
|
|
45
39
|
}
|
|
@@ -47,6 +41,12 @@ async function getGitState(projectPath) {
|
|
|
47
41
|
return null;
|
|
48
42
|
}
|
|
49
43
|
}
|
|
44
|
+
function countGitPathLines(stdout) {
|
|
45
|
+
return stdout
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map((line) => line.trim())
|
|
48
|
+
.filter(Boolean).length;
|
|
49
|
+
}
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// CI state helpers
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
@@ -37,6 +37,7 @@ import { handleIdeConfig } from '../ide-config-handler.js';
|
|
|
37
37
|
import { IdeTargetEnum, IdeConfigScopeEnum } from '../schemas/ide-config.js';
|
|
38
38
|
import { handleReconcileSpecLiving } from '../reconcile-spec-living-handler.js';
|
|
39
39
|
import { handleSecurityReport } from '../security-report-handler.js';
|
|
40
|
+
import { handleFeedbackStatus, handleResolveFeedback, handleSubmitFeedback, handleSyncFeedback, handleTriageFeedback, } from '../feedback-handler.js';
|
|
40
41
|
import { SecurityReportTimeRangeEnum, SecurityReportFormatEnum, } from '../schemas/runtime-security.js';
|
|
41
42
|
import { RenderSpecForProviderInputSchema, handleRenderSpecForProvider, } from '../render-spec-for-provider.js';
|
|
42
43
|
import { handleTriageRequest, TriageRequestInputSchema } from '../triage-request.js';
|
|
@@ -750,6 +751,102 @@ const coreToolsRegistry = [
|
|
|
750
751
|
wrap: 'tracked',
|
|
751
752
|
group: 'core',
|
|
752
753
|
},
|
|
754
|
+
// ── feedback hub (SPEC-188, SPEC-1100) ───────────────────────────────────
|
|
755
|
+
{
|
|
756
|
+
name: 'submit_feedback',
|
|
757
|
+
description: 'Submit user feedback or a bug report to Planu Feedback Hub. Always persists locally first, then attempts privacy-bounded remote registration with the central feedback intake.',
|
|
758
|
+
schema: {
|
|
759
|
+
type: z
|
|
760
|
+
.enum(['bug', 'feature', 'suggestion', 'dx'])
|
|
761
|
+
.describe('Feedback type: bug | feature | suggestion | dx'),
|
|
762
|
+
title: z.string().min(1).max(200).describe('Short feedback title'),
|
|
763
|
+
description: z.string().min(1).max(5000).describe('Detailed feedback description'),
|
|
764
|
+
context: z
|
|
765
|
+
.object({
|
|
766
|
+
projectPath: z.string().max(4096).optional(),
|
|
767
|
+
currentFile: z.string().max(4096).optional(),
|
|
768
|
+
errorMessage: z.string().max(2000).optional(),
|
|
769
|
+
stack: z.string().max(8000).optional(),
|
|
770
|
+
})
|
|
771
|
+
.optional()
|
|
772
|
+
.describe('Optional local-only context; sensitive path fields are not sent remotely.'),
|
|
773
|
+
},
|
|
774
|
+
handler: async (args) => handleSubmitFeedback(args),
|
|
775
|
+
annotations: {
|
|
776
|
+
title: 'Submit Feedback',
|
|
777
|
+
readOnlyHint: false,
|
|
778
|
+
destructiveHint: false,
|
|
779
|
+
openWorldHint: true,
|
|
780
|
+
},
|
|
781
|
+
wrap: 'tracked',
|
|
782
|
+
group: 'feedback',
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: 'triage_feedback',
|
|
786
|
+
description: 'Group pending local feedback by similarity so maintainers can decide whether to create specs, merge duplicates, or dismiss items.',
|
|
787
|
+
schema: {
|
|
788
|
+
filter: z
|
|
789
|
+
.enum(['bug', 'feature', 'suggestion', 'dx'])
|
|
790
|
+
.optional()
|
|
791
|
+
.describe('Optional feedback type filter'),
|
|
792
|
+
},
|
|
793
|
+
handler: async (args) => Promise.resolve(handleTriageFeedback(args)),
|
|
794
|
+
annotations: { title: 'Triage Feedback', readOnlyHint: true },
|
|
795
|
+
wrap: 'tracked',
|
|
796
|
+
group: 'feedback',
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: 'resolve_feedback',
|
|
800
|
+
description: 'Mark a local feedback item as resolved and optionally link it to a Planu spec, PR URL, and resolution notes.',
|
|
801
|
+
schema: {
|
|
802
|
+
feedbackId: z.string().min(1).max(50).describe('Local feedback ID, e.g. FB-001'),
|
|
803
|
+
specId: z.string().max(50).optional().describe('Optional linked Planu spec ID'),
|
|
804
|
+
prUrl: z.url().optional().describe('Optional pull request URL'),
|
|
805
|
+
notes: z.string().max(2000).optional().describe('Optional resolution notes'),
|
|
806
|
+
},
|
|
807
|
+
handler: async (args) => handleResolveFeedback(args),
|
|
808
|
+
annotations: { title: 'Resolve Feedback', readOnlyHint: false, destructiveHint: false },
|
|
809
|
+
wrap: 'tracked',
|
|
810
|
+
group: 'feedback',
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
name: 'sync_feedback',
|
|
814
|
+
description: 'Retry remote Supabase registration for local feedback entries whose delivery is pending or failed. Does not accept remote row IDs or perform direct SQL/Data API writes.',
|
|
815
|
+
schema: {
|
|
816
|
+
projectPath: z.string().max(4096).optional().describe('Absolute path to project root'),
|
|
817
|
+
includeSynced: z
|
|
818
|
+
.boolean()
|
|
819
|
+
.optional()
|
|
820
|
+
.describe('When true, also reprocess entries already marked synced. Default false.'),
|
|
821
|
+
limit: z
|
|
822
|
+
.number()
|
|
823
|
+
.int()
|
|
824
|
+
.positive()
|
|
825
|
+
.max(500)
|
|
826
|
+
.optional()
|
|
827
|
+
.describe('Maximum number of local entries to process.'),
|
|
828
|
+
},
|
|
829
|
+
handler: async (args) => handleSyncFeedback(args),
|
|
830
|
+
annotations: {
|
|
831
|
+
title: 'Sync Feedback',
|
|
832
|
+
readOnlyHint: false,
|
|
833
|
+
destructiveHint: false,
|
|
834
|
+
openWorldHint: true,
|
|
835
|
+
},
|
|
836
|
+
wrap: 'tracked',
|
|
837
|
+
group: 'feedback',
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: 'feedback_status',
|
|
841
|
+
description: 'Show aggregate local feedback totals and remote delivery health without exposing descriptions, file paths, user identifiers, or secrets.',
|
|
842
|
+
schema: {
|
|
843
|
+
projectPath: z.string().max(4096).optional().describe('Absolute path to project root'),
|
|
844
|
+
},
|
|
845
|
+
handler: async (args) => Promise.resolve(handleFeedbackStatus(args)),
|
|
846
|
+
annotations: { title: 'Feedback Status', readOnlyHint: true },
|
|
847
|
+
wrap: 'tracked',
|
|
848
|
+
group: 'feedback',
|
|
849
|
+
},
|
|
753
850
|
];
|
|
754
851
|
export default coreToolsRegistry;
|
|
755
852
|
//# sourceMappingURL=core-tools.js.map
|
|
@@ -22,6 +22,9 @@ export function withToolTracking(server) {
|
|
|
22
22
|
// Return a wrapper function that calls the original method and captures the result
|
|
23
23
|
return (...args) => {
|
|
24
24
|
const name = args[0];
|
|
25
|
+
if (registeredTools.has(name)) {
|
|
26
|
+
throw new Error(`Duplicate MCP tool registration: ${name}`);
|
|
27
|
+
}
|
|
25
28
|
const originalMethod = Reflect.get(target, prop, receiver);
|
|
26
29
|
const registered = originalMethod.apply(target, args);
|
|
27
30
|
registeredTools.set(name, registered);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type NewCodeGateResult } from '../engine/ai-assurance/index.js';
|
|
2
|
+
export declare function runAssuranceGates(projectPath: string): {
|
|
3
|
+
newCode: NewCodeGateResult;
|
|
4
|
+
};
|
|
5
|
+
export declare function checkPlanuUncommittedChanges(projectPath: string): boolean;
|
|
6
|
+
//# sourceMappingURL=validate-assurance.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// tools/validate-assurance.ts — Validate assurance and local git helpers.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
|
|
5
|
+
export function runAssuranceGates(projectPath) {
|
|
6
|
+
return {
|
|
7
|
+
newCode: evaluateNewCodeGate(readChangedSpecLogicFiles(projectPath)),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function readChangedSpecLogicFiles(projectPath) {
|
|
11
|
+
let changedFiles;
|
|
12
|
+
try {
|
|
13
|
+
const output = execFileSync('git', [
|
|
14
|
+
'-C',
|
|
15
|
+
projectPath,
|
|
16
|
+
'diff',
|
|
17
|
+
'--name-only',
|
|
18
|
+
'HEAD',
|
|
19
|
+
'--',
|
|
20
|
+
'src/tools/create-spec',
|
|
21
|
+
'src/tools/validate.ts',
|
|
22
|
+
'src/engine',
|
|
23
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
24
|
+
changedFiles = output
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter((line) => line.endsWith('.ts'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return changedFiles.flatMap((path) => {
|
|
33
|
+
try {
|
|
34
|
+
return [{ path, content: readFileSync(`${projectPath}/${path}`, 'utf-8') }];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function checkPlanuUncommittedChanges(projectPath) {
|
|
42
|
+
try {
|
|
43
|
+
const output = execFileSync('git', ['status', '--porcelain', 'planu/'], {
|
|
44
|
+
cwd: projectPath,
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
timeout: 5_000,
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
return output.trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=validate-assurance.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GraphCoverageReport } from '../types/project-knowledge-graph.js';
|
|
2
|
+
export declare function buildGraphCoverageReport(args: {
|
|
3
|
+
projectId: string;
|
|
4
|
+
projectPath: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
}): Promise<GraphCoverageReport>;
|
|
7
|
+
export declare function formatGraphCoverageText(report: GraphCoverageReport): string;
|
|
8
|
+
//# sourceMappingURL=validate-graph-coverage.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// tools/validate-graph-coverage.ts — Graph coverage helpers for validate.
|
|
2
|
+
import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
|
|
3
|
+
function graphCoverageGaps(slice) {
|
|
4
|
+
if (slice === null) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const criteria = slice.nodes.filter((node) => node.type === 'criterion');
|
|
8
|
+
return criteria
|
|
9
|
+
.filter((criterion) => {
|
|
10
|
+
const outgoing = slice.edges.filter((edge) => edge.from === criterion.id);
|
|
11
|
+
return !outgoing.some((edge) => edge.type === 'implements' || edge.type === 'tests');
|
|
12
|
+
})
|
|
13
|
+
.map((criterion) => criterion.label)
|
|
14
|
+
.slice(0, 10);
|
|
15
|
+
}
|
|
16
|
+
export async function buildGraphCoverageReport(args) {
|
|
17
|
+
const graphSlice = await queryProjectGraphSlice(args).catch(() => null);
|
|
18
|
+
return {
|
|
19
|
+
freshness: graphSlice?.freshness,
|
|
20
|
+
gaps: graphCoverageGaps(graphSlice),
|
|
21
|
+
compactNodes: graphSlice?.nodes.length ?? 0,
|
|
22
|
+
compactEdges: graphSlice?.edges.length ?? 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function formatGraphCoverageText(report) {
|
|
26
|
+
return report.gaps.length > 0
|
|
27
|
+
? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
|
|
28
|
+
: '';
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=validate-graph-coverage.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InteractiveQuestion } from '../types/clarification.js';
|
|
2
|
+
import type { ToolResult } from '../types/index.js';
|
|
3
|
+
export declare function validateStrictLayoutOrError(args: {
|
|
4
|
+
projectPath: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
specPath?: string;
|
|
7
|
+
}): Promise<ToolResult | null>;
|
|
8
|
+
export declare function buildValidateFailureFallback(failedCount: number): InteractiveQuestion;
|
|
9
|
+
//# sourceMappingURL=validate-helpers.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export async function validateStrictLayoutOrError(args) {
|
|
2
|
+
try {
|
|
3
|
+
const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
|
|
4
|
+
const layout = await validateStrictPlanuLayout(args.projectPath, {
|
|
5
|
+
specPath: args.specPath,
|
|
6
|
+
includeRoot: false,
|
|
7
|
+
});
|
|
8
|
+
if (layout.ok) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: 'text',
|
|
15
|
+
text: `Strict Planu layout validation failed for ${args.specId}.\n\n` +
|
|
16
|
+
`Non-canonical paths:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}\n\n` +
|
|
17
|
+
`Canonical contract:\n${layout.contract}`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
isError: true,
|
|
21
|
+
structuredContent: {
|
|
22
|
+
error: 'strict_planu_layout_violation',
|
|
23
|
+
offenders: layout.offenders,
|
|
24
|
+
contract: layout.contract,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function buildValidateFailureFallback(failedCount) {
|
|
33
|
+
return {
|
|
34
|
+
header: 'Failures',
|
|
35
|
+
question: `${failedCount} criteria failed. How do you want to proceed?`,
|
|
36
|
+
multiSelect: false,
|
|
37
|
+
options: [
|
|
38
|
+
{
|
|
39
|
+
label: 're-implement',
|
|
40
|
+
description: 'Fix the failing criteria and run validate again (Recommended)',
|
|
41
|
+
},
|
|
42
|
+
{ label: 'ignore', description: 'Ignore failures and proceed (not recommended)' },
|
|
43
|
+
{ label: 'mark-manual', description: 'Mark as manually reviewed — will bypass auto-check' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=validate-helpers.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { formatLintFailureDiagnostics, resolveProjectCommandPlan, runProjectCommandPlan, } from './validate-runtime.js';
|
|
2
|
+
/** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
|
|
3
|
+
const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
|
|
4
|
+
const LINT_CHECK_TIMEOUT_MS = 50_000;
|
|
5
|
+
function isSafeCommand(cmd) {
|
|
6
|
+
return SAFE_COMMAND_RE.test(cmd);
|
|
7
|
+
}
|
|
8
|
+
function isCommandTimeout(err) {
|
|
9
|
+
if (!(err instanceof Error)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const errorWithProcessFields = err;
|
|
13
|
+
return (errorWithProcessFields.signal === 'SIGTERM' ||
|
|
14
|
+
errorWithProcessFields.killed === true ||
|
|
15
|
+
errorWithProcessFields.code === 'ETIMEDOUT' ||
|
|
16
|
+
/timed out|timeout/i.test(err.message));
|
|
17
|
+
}
|
|
18
|
+
function commandOutput(err) {
|
|
19
|
+
if (!(err instanceof Error)) {
|
|
20
|
+
return String(err);
|
|
21
|
+
}
|
|
22
|
+
const errorWithOutput = err;
|
|
23
|
+
return [errorWithOutput.stdout, errorWithOutput.stderr]
|
|
24
|
+
.filter((chunk) => chunk !== undefined)
|
|
25
|
+
.map((chunk) => String(chunk))
|
|
26
|
+
.join('\n')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
function parseLintIssueCount(output) {
|
|
30
|
+
const m = /(\d+)\s+problem/i.exec(output);
|
|
31
|
+
if (m) {
|
|
32
|
+
return parseInt(m[1] ?? '0', 10);
|
|
33
|
+
}
|
|
34
|
+
return output.split('\n').filter((l) => l.trim().length > 0).length;
|
|
35
|
+
}
|
|
36
|
+
export function runLintCheck(projectPath, lintCommand) {
|
|
37
|
+
if (lintCommand !== null && !isSafeCommand(lintCommand)) {
|
|
38
|
+
console.warn(`[Planu] validate: lintCommand contains unsafe characters — skipping execution`);
|
|
39
|
+
return {
|
|
40
|
+
passed: true,
|
|
41
|
+
command: lintCommand,
|
|
42
|
+
issueCount: 0,
|
|
43
|
+
output: '(skipped — unsafe command)',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const command = lintCommand ?? 'pnpm lint';
|
|
47
|
+
const plan = resolveProjectCommandPlan(projectPath, command);
|
|
48
|
+
try {
|
|
49
|
+
runProjectCommandPlan(plan, projectPath, LINT_CHECK_TIMEOUT_MS);
|
|
50
|
+
return { passed: true, command, issueCount: 0, output: '' };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (isCommandTimeout(err)) {
|
|
54
|
+
const seconds = Math.round(LINT_CHECK_TIMEOUT_MS / 1000);
|
|
55
|
+
const output = `Lint command timed out after ${String(seconds)}s and was skipped so validate can complete within MCP request limits. ` +
|
|
56
|
+
`Run \`${plan.executedCommand}\` manually before marking the spec done.`;
|
|
57
|
+
console.warn(`[Planu] lintCheck timed out: command="${plan.executedCommand}" cwd="${projectPath}" timeoutMs=${String(LINT_CHECK_TIMEOUT_MS)}`);
|
|
58
|
+
return { passed: true, command, issueCount: 0, output };
|
|
59
|
+
}
|
|
60
|
+
const raw = commandOutput(err);
|
|
61
|
+
const output = formatLintFailureDiagnostics(plan, raw);
|
|
62
|
+
const issueCount = parseLintIssueCount(raw.trim());
|
|
63
|
+
console.warn(`[Planu] lintCheck failed: command="${plan.executedCommand}" cwd="${projectPath}" issues=${String(issueCount)}`);
|
|
64
|
+
return { passed: false, command, issueCount, output };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=validate-lint.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MinimalImplementationReport } from '../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function buildMinimalityReport(args: {
|
|
3
|
+
projectPath: string;
|
|
4
|
+
specId: string;
|
|
5
|
+
risk?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
missing: string[];
|
|
8
|
+
extra: string[];
|
|
9
|
+
qualityIssues: {
|
|
10
|
+
file: string;
|
|
11
|
+
rule: string;
|
|
12
|
+
message: string;
|
|
13
|
+
suggestion?: string;
|
|
14
|
+
}[];
|
|
15
|
+
}): Promise<MinimalImplementationReport | null>;
|
|
16
|
+
//# sourceMappingURL=validate-minimality.d.ts.map
|