@planu/cli 4.9.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 +13 -0
- package/dist/config/license-plans.json +5 -0
- package/dist/config/registries/hosts/codex.json +3 -4
- package/dist/engine/evidence-index/index-builder.js +38 -12
- 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/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/package.json +12 -12
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [4.10.0] - 2026-07-01
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat(SPEC-1100): add reliable feedback sync
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
- fix(SPEC-1102): honor release preflight test skip
|
|
8
|
+
- fix(SPEC-1101): harden validate runtime and status
|
|
9
|
+
|
|
10
|
+
### Chores
|
|
11
|
+
- chore(deps): refresh direct dependencies
|
|
12
|
+
|
|
13
|
+
|
|
1
14
|
## [4.9.0] - 2026-06-30
|
|
2
15
|
|
|
3
16
|
### Features
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"export_spec",
|
|
93
93
|
"expose_spec_as_prompt",
|
|
94
94
|
"facilitate",
|
|
95
|
+
"feedback_status",
|
|
95
96
|
"gc_data_projects",
|
|
96
97
|
"generate_api_client",
|
|
97
98
|
"generate_attestation",
|
|
@@ -163,6 +164,7 @@
|
|
|
163
164
|
"render_spec_for_provider",
|
|
164
165
|
"repair_frontmatter_drift",
|
|
165
166
|
"request_changes",
|
|
167
|
+
"resolve_feedback",
|
|
166
168
|
"reverse_engineer",
|
|
167
169
|
"rewrite_criteria_ears",
|
|
168
170
|
"run_mutation_hints",
|
|
@@ -189,13 +191,16 @@
|
|
|
189
191
|
"suggest_token_optimizer",
|
|
190
192
|
"suggest_tooling",
|
|
191
193
|
"summarize_spec",
|
|
194
|
+
"submit_feedback",
|
|
192
195
|
"sync_ai_configs",
|
|
196
|
+
"sync_feedback",
|
|
193
197
|
"sync_spec_state",
|
|
194
198
|
"tdd_scaffold",
|
|
195
199
|
"tdd_status",
|
|
196
200
|
"telemetry_health",
|
|
197
201
|
"token_optimizer_status",
|
|
198
202
|
"transform_code",
|
|
203
|
+
"triage_feedback",
|
|
199
204
|
"triage_request",
|
|
200
205
|
"type_safety_gate",
|
|
201
206
|
"unregister_project_path",
|
|
@@ -294,6 +294,8 @@
|
|
|
294
294
|
"submit_feedback",
|
|
295
295
|
"triage_feedback",
|
|
296
296
|
"resolve_feedback",
|
|
297
|
+
"sync_feedback",
|
|
298
|
+
"feedback_status",
|
|
297
299
|
"delete_spec",
|
|
298
300
|
"delete_project",
|
|
299
301
|
"delete_pattern",
|
|
@@ -373,10 +375,7 @@
|
|
|
373
375
|
"analyze_performance_impact",
|
|
374
376
|
"recommend_model"
|
|
375
377
|
],
|
|
376
|
-
"unsupportedTools": [
|
|
377
|
-
"challenge_spec",
|
|
378
|
-
"red_team"
|
|
379
|
-
],
|
|
378
|
+
"unsupportedTools": ["challenge_spec", "red_team"],
|
|
380
379
|
"restrictedTools": {
|
|
381
380
|
"challenge_spec": "Requires interactive UI — use planu_facilitate instead",
|
|
382
381
|
"approve_spec": "Requires reviewer token — use planu_facilitate instead"
|
|
@@ -37,16 +37,16 @@ async function collectEvidence(args) {
|
|
|
37
37
|
records.push(record('scenario', row.scenario, 'valid', 'traceability-matrix'));
|
|
38
38
|
}
|
|
39
39
|
for (const test of row.testEvidence ?? []) {
|
|
40
|
-
records.push(await pathRecord('test', test, 'traceability-matrix', args));
|
|
40
|
+
records.push(await pathRecord('test', test, 'traceability-matrix', args, 'testEvidence'));
|
|
41
41
|
}
|
|
42
42
|
for (const contract of row.contractEvidence ?? []) {
|
|
43
|
-
records.push(await pathRecord('contract', contract, 'traceability-matrix', args));
|
|
43
|
+
records.push(await pathRecord('contract', contract, 'traceability-matrix', args, 'contractEvidence'));
|
|
44
44
|
}
|
|
45
45
|
if (row.manualEvidence) {
|
|
46
46
|
records.push(record('manual', row.manualEvidence, 'valid', 'traceability-matrix'));
|
|
47
47
|
}
|
|
48
48
|
for (const changedFile of row.changedFiles) {
|
|
49
|
-
records.push(await pathRecord('changed-file', changedFile, 'traceability-matrix', args));
|
|
49
|
+
records.push(await pathRecord('changed-file', changedFile, 'traceability-matrix', args, 'changedFiles'));
|
|
50
50
|
}
|
|
51
51
|
if (row.validationEvidence) {
|
|
52
52
|
records.push(validationRecord(row.validationEvidence));
|
|
@@ -57,7 +57,7 @@ async function collectEvidence(args) {
|
|
|
57
57
|
}
|
|
58
58
|
for (const validation of args.contractValidations) {
|
|
59
59
|
if (validation.reportPath) {
|
|
60
|
-
records.push(await pathRecord('contract', validation.reportPath, `contract-validation:${validation.kind}`, args));
|
|
60
|
+
records.push(await pathRecord('contract', validation.reportPath, `contract-validation:${validation.kind}`, args, 'contractValidation.reportPath'));
|
|
61
61
|
}
|
|
62
62
|
if (validation.summary) {
|
|
63
63
|
records.push({
|
|
@@ -95,36 +95,62 @@ function parseValidationEvidence(value) {
|
|
|
95
95
|
return { command: value };
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
async function pathRecord(kind, value, source, args) {
|
|
99
|
-
const safe = resolveSafeProjectPath(value, args.projectPath);
|
|
98
|
+
async function pathRecord(kind, value, source, args, fieldName) {
|
|
99
|
+
const safe = resolveSafeProjectPath(value, args.projectPath, fieldName);
|
|
100
100
|
if (!safe.ok) {
|
|
101
101
|
args.diagnostics.push(`${value}: ${safe.reason}`);
|
|
102
102
|
return record(kind, value, 'invalid', source, safe.reason);
|
|
103
103
|
}
|
|
104
104
|
if (!args.projectPath) {
|
|
105
|
-
return record(kind, value, 'missing', source, 'projectPath unavailable');
|
|
105
|
+
return record(kind, value, 'missing', source, evidencePathReason(fieldName, 'projectPath unavailable'));
|
|
106
106
|
}
|
|
107
107
|
const exists = await stat(safe.path)
|
|
108
108
|
.then(() => true)
|
|
109
109
|
.catch(() => false);
|
|
110
|
-
|
|
110
|
+
const missingReason = evidencePathReason(fieldName, `Missing path: ${value}`);
|
|
111
|
+
return record(kind, value, exists ? 'valid' : 'stale', source, exists ? undefined : missingReason);
|
|
111
112
|
}
|
|
112
|
-
function resolveSafeProjectPath(value, projectPath) {
|
|
113
|
+
function resolveSafeProjectPath(value, projectPath, fieldName) {
|
|
113
114
|
if (value.trim().length === 0) {
|
|
114
|
-
return { ok: false, reason: '
|
|
115
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Empty path.') };
|
|
116
|
+
}
|
|
117
|
+
if (looksLikeShellCommand(value)) {
|
|
118
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Commands are not valid paths.') };
|
|
119
|
+
}
|
|
120
|
+
if (looksLikeGlob(value)) {
|
|
121
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Globs are not valid paths.') };
|
|
115
122
|
}
|
|
116
123
|
if (isAbsolute(value)) {
|
|
117
|
-
return { ok: false, reason: '
|
|
124
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Absolute paths are not allowed.') };
|
|
118
125
|
}
|
|
119
126
|
const normalized = normalize(value);
|
|
120
127
|
if (normalized.startsWith('..') || normalized.includes('/../')) {
|
|
121
|
-
return { ok: false, reason: '
|
|
128
|
+
return { ok: false, reason: evidencePathReason(fieldName, 'Path traversal is not allowed.') };
|
|
122
129
|
}
|
|
123
130
|
if (!projectPath) {
|
|
124
131
|
return { ok: true, path: normalized };
|
|
125
132
|
}
|
|
126
133
|
return { ok: true, path: join(projectPath, normalized) };
|
|
127
134
|
}
|
|
135
|
+
function evidencePathReason(fieldName, detail) {
|
|
136
|
+
if (fieldName === 'testEvidence') {
|
|
137
|
+
return `testEvidence must contain existing file paths only; put command output in validationEvidence. ${detail}`;
|
|
138
|
+
}
|
|
139
|
+
if (fieldName === 'changedFiles') {
|
|
140
|
+
return `changedFiles must contain existing repository-relative paths only; no globs or commands. ${detail}`;
|
|
141
|
+
}
|
|
142
|
+
return `Evidence path must be an existing repository-relative path. ${detail}`;
|
|
143
|
+
}
|
|
144
|
+
function looksLikeShellCommand(value) {
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
if (/\s/.test(trimmed) && !/\.[A-Za-z0-9]+$/.test(trimmed)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return /^(?:pnpm|npm|yarn|bun|npx|node|tsx|tsc|vitest|jest|git|bash|sh)\b/.test(trimmed);
|
|
150
|
+
}
|
|
151
|
+
function looksLikeGlob(value) {
|
|
152
|
+
return /[*?[\]{}]/.test(value);
|
|
153
|
+
}
|
|
128
154
|
function record(kind, value, status, source, reason) {
|
|
129
155
|
return { kind, value, status, source, ...(reason ? { reason } : {}) };
|
|
130
156
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { RemoteFeedbackPayload } from '../types/feedback.js';
|
|
1
|
+
import type { FeedbackRemoteResult, RemoteFeedbackPayload } from '../types/feedback.js';
|
|
2
|
+
export declare const DEFAULT_FEEDBACK_API = "https://canusqigtzldstnjddio.supabase.co/functions/v1/license-api/feedback";
|
|
2
3
|
export type { RemoteFeedbackPayload };
|
|
4
|
+
export declare function resolveFeedbackEndpoint(endpoint?: string): string;
|
|
5
|
+
export declare function sanitizeRemoteFeedbackPayload(payload: RemoteFeedbackPayload): RemoteFeedbackPayload;
|
|
6
|
+
export declare function deliverFeedbackRemote(payload: RemoteFeedbackPayload, endpoint?: string): Promise<FeedbackRemoteResult>;
|
|
3
7
|
/**
|
|
4
8
|
* Send feedback to central Supabase store (fire-and-forget).
|
|
5
9
|
* Never throws. Returns true if sent successfully, false otherwise.
|
|
@@ -1,27 +1,86 @@
|
|
|
1
1
|
// storage/feedback-remote.ts — Remote feedback relay to Supabase (fire-and-forget)
|
|
2
2
|
// No PII is ever sent: no project paths, no user names, no emails.
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export const DEFAULT_FEEDBACK_API = 'https://canusqigtzldstnjddio.supabase.co/functions/v1/license-api/feedback';
|
|
4
|
+
export function resolveFeedbackEndpoint(endpoint) {
|
|
5
|
+
return endpoint ?? process.env.PLANU_FEEDBACK_ENDPOINT ?? DEFAULT_FEEDBACK_API;
|
|
6
|
+
}
|
|
7
|
+
function redactSensitiveText(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]')
|
|
10
|
+
.replace(/(?:\/Users|\/home|\/var|\/tmp|[A-Za-z]:\\)[^\s)'"`]+/g, '[redacted-path]')
|
|
11
|
+
.replace(/```[\s\S]*?```/g, '[redacted-code-block]')
|
|
12
|
+
.replace(/\b(?:const|let|var|function|class|import|export)\s+[^\n]+/g, '[redacted-code]');
|
|
13
|
+
}
|
|
14
|
+
export function sanitizeRemoteFeedbackPayload(payload) {
|
|
15
|
+
return {
|
|
16
|
+
...payload,
|
|
17
|
+
title: redactSensitiveText(payload.title),
|
|
18
|
+
description: redactSensitiveText(payload.description),
|
|
19
|
+
...(payload.stack !== undefined ? { stack: redactSensitiveText(payload.stack) } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function deliverFeedbackRemote(payload, endpoint) {
|
|
23
|
+
const resolvedEndpoint = resolveFeedbackEndpoint(endpoint);
|
|
24
|
+
let url;
|
|
9
25
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
url = new URL(resolvedEndpoint);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
status: 'skipped',
|
|
32
|
+
endpoint: resolvedEndpoint,
|
|
33
|
+
error: 'Invalid feedback endpoint URL.',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (url.protocol !== 'https:' && url.hostname !== 'localhost' && url.hostname !== '127.0.0.1') {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 'skipped',
|
|
40
|
+
endpoint: resolvedEndpoint,
|
|
41
|
+
error: 'Feedback endpoint must use HTTPS.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
controller.abort();
|
|
47
|
+
}, 5000);
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(resolvedEndpoint, {
|
|
15
50
|
method: 'POST',
|
|
16
51
|
headers: { 'Content-Type': 'application/json' },
|
|
17
|
-
body: JSON.stringify(payload),
|
|
52
|
+
body: JSON.stringify(sanitizeRemoteFeedbackPayload(payload)),
|
|
18
53
|
signal: controller.signal,
|
|
19
54
|
});
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
return { ok: true, status: 'synced', endpoint: resolvedEndpoint };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
status: 'failed',
|
|
61
|
+
endpoint: resolvedEndpoint,
|
|
62
|
+
error: `Remote feedback endpoint returned HTTP ${res.status}.`,
|
|
63
|
+
};
|
|
22
64
|
}
|
|
23
|
-
catch {
|
|
24
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
status: 'failed',
|
|
70
|
+
endpoint: resolvedEndpoint,
|
|
71
|
+
error: message || 'Remote feedback request failed.',
|
|
72
|
+
};
|
|
25
73
|
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Send feedback to central Supabase store (fire-and-forget).
|
|
80
|
+
* Never throws. Returns true if sent successfully, false otherwise.
|
|
81
|
+
*/
|
|
82
|
+
export async function sendFeedbackRemote(payload) {
|
|
83
|
+
const result = await deliverFeedbackRemote(payload);
|
|
84
|
+
return result.ok;
|
|
26
85
|
}
|
|
27
86
|
//# sourceMappingURL=feedback-remote.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FeedbackEntry, FeedbackFilter, FeedbackSummary } from '../types/index.js';
|
|
1
|
+
import type { FeedbackEntry, FeedbackFilter, FeedbackSummary, FeedbackRemoteHealth, SyncFeedbackInput, SyncFeedbackSummary } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Submit a new feedback entry. Auto-generates ID, timestamp, and version.
|
|
4
4
|
*/
|
|
@@ -19,6 +19,8 @@ export declare function resolveFeedback(dataDir: string, id: string, specId?: st
|
|
|
19
19
|
* Compute summary statistics across all feedback.
|
|
20
20
|
*/
|
|
21
21
|
export declare function getSummary(dataDir: string): FeedbackSummary;
|
|
22
|
+
export declare function getFeedbackRemoteHealth(dataDir: string): FeedbackRemoteHealth;
|
|
23
|
+
export declare function syncFeedbackRemote(dataDir: string, input?: SyncFeedbackInput): Promise<SyncFeedbackSummary>;
|
|
22
24
|
/**
|
|
23
25
|
* Find feedback entries with title similar to the given string.
|
|
24
26
|
* Uses Jaccard word overlap. Threshold defaults to 0.3.
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|