@fenglimg/fabric-cli 2.0.0-rc.25 → 2.0.0-rc.27
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/dist/chunk-PWLW3B57.js +18 -0
- package/dist/{chunk-STLR2GHP.js → chunk-SRX7WZUG.js} +1 -1
- package/dist/{chunk-AXKII55Y.js → chunk-XEGXQOOJ.js} +75 -1
- package/dist/{config-XGUUAYX6.js → config-5CH4EJQ2.js} +2 -2
- package/dist/{doctor-DXKPYPRC.js → doctor-ZIQXN2T2.js} +67 -62
- package/dist/index.js +8 -8
- package/dist/{install-S2J76N2B.js → install-UJOFZUYF.js} +4 -4
- package/dist/{plan-context-hint-KPGOW3QC.js → plan-context-hint-CXTLNVSV.js} +17 -2
- package/dist/{serve-NPCI342P.js → serve-U3TPWDOB.js} +1 -1
- package/dist/{uninstall-MQM6NUFM.js → uninstall-O3PXESM2.js} +2 -2
- package/package.json +3 -3
- package/templates/hooks/fabric-hint.cjs +104 -12
- package/templates/hooks/knowledge-hint-narrow.cjs +41 -2
- package/templates/hooks/lib/banner-i18n.cjs +27 -0
- package/templates/hooks/lib/cite-line-parser.cjs +42 -8
- package/templates/skills/fabric-archive/SKILL.md +19 -4
- package/dist/chunk-6ICJICVU.js +0 -10
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/i18n.ts
|
|
4
|
+
import {
|
|
5
|
+
createTranslator,
|
|
6
|
+
detectNodeLocale,
|
|
7
|
+
resolveFabricLocale
|
|
8
|
+
} from "@fenglimg/fabric-shared";
|
|
9
|
+
var locale = detectNodeLocale();
|
|
10
|
+
var t = createTranslator(locale);
|
|
11
|
+
function getDoctorTranslator(projectRoot) {
|
|
12
|
+
return createTranslator(resolveFabricLocale(projectRoot));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
t,
|
|
17
|
+
getDoctorTranslator
|
|
18
|
+
};
|
|
@@ -513,9 +513,83 @@ async function copyTextIdempotent(step, source, target) {
|
|
|
513
513
|
await atomicWriteText2(target, source);
|
|
514
514
|
return { step, path: target, status: "written" };
|
|
515
515
|
}
|
|
516
|
+
var FABRIC_HOOK_SCRIPT_BASENAMES = /* @__PURE__ */ new Set([
|
|
517
|
+
"fabric-hint.cjs",
|
|
518
|
+
"knowledge-hint-broad.cjs",
|
|
519
|
+
"knowledge-hint-narrow.cjs",
|
|
520
|
+
// rc.5 TASK-010 rename — old hook scripts that pre-upgrade workspaces
|
|
521
|
+
// may still have registered. Sweeping them prevents the double-fire
|
|
522
|
+
// documented in audit §2.6.
|
|
523
|
+
"archive-hint.cjs"
|
|
524
|
+
]);
|
|
525
|
+
function commandBasename(command) {
|
|
526
|
+
const trimmed = command.trim().replace(/^"+|"+$/g, "");
|
|
527
|
+
const match = /([^/\\]+\.cjs)$/u.exec(trimmed);
|
|
528
|
+
return match === null ? null : match[1];
|
|
529
|
+
}
|
|
530
|
+
function stripStaleHookEntries(existing, arrayAppendPaths) {
|
|
531
|
+
const swept = JSON.parse(JSON.stringify(existing));
|
|
532
|
+
let removed = 0;
|
|
533
|
+
for (const dottedPath of arrayAppendPaths) {
|
|
534
|
+
const segments = dottedPath.split(".");
|
|
535
|
+
let cursor = swept;
|
|
536
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
537
|
+
const seg = segments[i];
|
|
538
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
539
|
+
cursor = void 0;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
cursor = cursor[seg];
|
|
543
|
+
}
|
|
544
|
+
if (cursor === null || cursor === void 0 || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const finalSeg = segments[segments.length - 1];
|
|
548
|
+
const arr = cursor[finalSeg];
|
|
549
|
+
if (!Array.isArray(arr)) continue;
|
|
550
|
+
const filtered = [];
|
|
551
|
+
for (const item of arr) {
|
|
552
|
+
if (item === null || typeof item !== "object") {
|
|
553
|
+
filtered.push(item);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const entry = item;
|
|
557
|
+
const hooks = entry.hooks;
|
|
558
|
+
let isFabricOwned = false;
|
|
559
|
+
if (Array.isArray(hooks)) {
|
|
560
|
+
for (const h of hooks) {
|
|
561
|
+
if (h !== null && typeof h === "object") {
|
|
562
|
+
const cmd = h.command;
|
|
563
|
+
if (typeof cmd === "string") {
|
|
564
|
+
const base = commandBasename(cmd);
|
|
565
|
+
if (base !== null && FABRIC_HOOK_SCRIPT_BASENAMES.has(base)) {
|
|
566
|
+
isFabricOwned = true;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (!isFabricOwned && typeof entry.command === "string") {
|
|
574
|
+
const base = commandBasename(entry.command);
|
|
575
|
+
if (base !== null && FABRIC_HOOK_SCRIPT_BASENAMES.has(base)) {
|
|
576
|
+
isFabricOwned = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (isFabricOwned) {
|
|
580
|
+
removed += 1;
|
|
581
|
+
} else {
|
|
582
|
+
filtered.push(item);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
cursor[finalSeg] = filtered;
|
|
586
|
+
}
|
|
587
|
+
return { swept, removed };
|
|
588
|
+
}
|
|
516
589
|
async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
|
|
517
590
|
const existing = await readJsonObjectOrEmpty(target);
|
|
518
|
-
const
|
|
591
|
+
const { swept } = stripStaleHookEntries(existing, arrayAppendPaths);
|
|
592
|
+
const merged = deepMerge(swept, fragment, { arrayAppendPaths });
|
|
519
593
|
if (jsonEqual(existing, merged)) {
|
|
520
594
|
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
521
595
|
}
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
configCmd,
|
|
4
4
|
config_default,
|
|
5
5
|
installMcpClients
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SRX7WZUG.js";
|
|
7
7
|
import "./chunk-MF3OTILQ.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-PWLW3B57.js";
|
|
9
9
|
export {
|
|
10
10
|
configCmd,
|
|
11
11
|
config_default as default,
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
symbol
|
|
7
7
|
} from "./chunk-G2CIOLD4.js";
|
|
8
8
|
import {
|
|
9
|
+
getDoctorTranslator,
|
|
9
10
|
t
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-PWLW3B57.js";
|
|
11
12
|
import {
|
|
12
13
|
resolveDevMode
|
|
13
14
|
} from "./chunk-COI5VDFU.js";
|
|
@@ -129,6 +130,7 @@ var doctorCommand = defineCommand({
|
|
|
129
130
|
async run({ args }) {
|
|
130
131
|
const workspaceRoot = process.cwd();
|
|
131
132
|
const resolution = resolveDevMode(args.target, workspaceRoot);
|
|
133
|
+
const dt = getDoctorTranslator(resolution.target);
|
|
132
134
|
try {
|
|
133
135
|
checkLockOrThrow(resolution.target);
|
|
134
136
|
} catch (err) {
|
|
@@ -145,7 +147,7 @@ var doctorCommand = defineCommand({
|
|
|
145
147
|
const archiveHistory = args["archive-history"] === true;
|
|
146
148
|
if (archiveHistory) {
|
|
147
149
|
if (fix || fixKnowledge || citeCoverage || enrichDesc) {
|
|
148
|
-
writeStderr(
|
|
150
|
+
writeStderr(dt("cli.doctor.errors.archive-history-mutex"));
|
|
149
151
|
process.exitCode = 1;
|
|
150
152
|
return;
|
|
151
153
|
}
|
|
@@ -154,7 +156,7 @@ var doctorCommand = defineCommand({
|
|
|
154
156
|
try {
|
|
155
157
|
sinceMs = parseSinceDuration(sinceInput);
|
|
156
158
|
} catch {
|
|
157
|
-
writeStderr(
|
|
159
|
+
writeStderr(dt("cli.doctor.errors.invalid-since", { input: sinceInput }));
|
|
158
160
|
process.exitCode = 1;
|
|
159
161
|
return;
|
|
160
162
|
}
|
|
@@ -164,13 +166,13 @@ var doctorCommand = defineCommand({
|
|
|
164
166
|
if (args.json === true) {
|
|
165
167
|
writeStdout(JSON.stringify(report2, null, 2));
|
|
166
168
|
} else {
|
|
167
|
-
renderArchiveHistoryReport(report2, sinceInput);
|
|
169
|
+
renderArchiveHistoryReport(report2, sinceInput, dt);
|
|
168
170
|
}
|
|
169
171
|
return;
|
|
170
172
|
}
|
|
171
173
|
if (enrichDesc) {
|
|
172
174
|
if (fix || fixKnowledge || citeCoverage) {
|
|
173
|
-
writeStderr(
|
|
175
|
+
writeStderr(dt("cli.doctor.errors.enrich-descriptions-mutex"));
|
|
174
176
|
process.exitCode = 1;
|
|
175
177
|
return;
|
|
176
178
|
}
|
|
@@ -183,13 +185,13 @@ var doctorCommand = defineCommand({
|
|
|
183
185
|
if (args.json === true) {
|
|
184
186
|
writeStdout(JSON.stringify(report2, null, 2));
|
|
185
187
|
} else {
|
|
186
|
-
renderEnrichDescriptionsReport(report2);
|
|
188
|
+
renderEnrichDescriptionsReport(report2, dt);
|
|
187
189
|
}
|
|
188
190
|
return;
|
|
189
191
|
}
|
|
190
192
|
if (citeCoverage) {
|
|
191
193
|
if (fix || fixKnowledge) {
|
|
192
|
-
writeStderr(
|
|
194
|
+
writeStderr(dt("cli.doctor.errors.cite-coverage-mutex"));
|
|
193
195
|
process.exitCode = 1;
|
|
194
196
|
return;
|
|
195
197
|
}
|
|
@@ -197,19 +199,19 @@ var doctorCommand = defineCommand({
|
|
|
197
199
|
try {
|
|
198
200
|
sinceMs = parseSinceDuration(args.since ?? "7d");
|
|
199
201
|
} catch {
|
|
200
|
-
writeStderr(
|
|
202
|
+
writeStderr(dt("cli.doctor.errors.invalid-since", { input: args.since ?? "7d" }));
|
|
201
203
|
process.exitCode = 1;
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
204
206
|
const clientFilter = args.client ?? "all";
|
|
205
207
|
if (!isValidClientFilter(clientFilter)) {
|
|
206
|
-
writeStderr(
|
|
208
|
+
writeStderr(dt("cli.doctor.errors.invalid-client", { input: clientFilter }));
|
|
207
209
|
process.exitCode = 1;
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
212
|
const layerFilter = args.layer ?? "all";
|
|
211
213
|
if (!isValidLayerFilter(layerFilter)) {
|
|
212
|
-
writeStderr(
|
|
214
|
+
writeStderr(dt("cli.doctor.errors.invalid-layer", { input: layerFilter }));
|
|
213
215
|
process.exitCode = 1;
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
@@ -218,11 +220,11 @@ var doctorCommand = defineCommand({
|
|
|
218
220
|
client: clientFilter,
|
|
219
221
|
layer: layerFilter
|
|
220
222
|
});
|
|
221
|
-
renderCiteCoverageReport(report2, args.json === true);
|
|
223
|
+
renderCiteCoverageReport(report2, args.json === true, dt);
|
|
222
224
|
return;
|
|
223
225
|
}
|
|
224
226
|
if (fixKnowledge && fix) {
|
|
225
|
-
writeStderr(
|
|
227
|
+
writeStderr(dt("cli.doctor.errors.fix-knowledge-fix-mutually-exclusive"));
|
|
226
228
|
process.exitCode = 1;
|
|
227
229
|
return;
|
|
228
230
|
}
|
|
@@ -263,11 +265,11 @@ var doctorCommand = defineCommand({
|
|
|
263
265
|
if (fixKnowledgeReport.aborted && fixKnowledgeReport.abort_reason !== void 0) {
|
|
264
266
|
writeStderr(fixKnowledgeReport.abort_reason);
|
|
265
267
|
}
|
|
266
|
-
renderFixKnowledgeMutations(fixKnowledgeReport);
|
|
268
|
+
renderFixKnowledgeMutations(fixKnowledgeReport, dt);
|
|
267
269
|
} else if (fixReport !== null) {
|
|
268
270
|
writeStdout(fixReport.message);
|
|
269
271
|
}
|
|
270
|
-
renderHumanReport(report);
|
|
272
|
+
renderHumanReport(report, dt);
|
|
271
273
|
}
|
|
272
274
|
await emitDoctorRunEventBestEffort(resolution.target, {
|
|
273
275
|
mode: fixKnowledge ? "fix-knowledge" : "lint",
|
|
@@ -290,21 +292,21 @@ var doctorCommand = defineCommand({
|
|
|
290
292
|
}
|
|
291
293
|
});
|
|
292
294
|
var doctor_default = doctorCommand;
|
|
293
|
-
function renderHumanReport(report) {
|
|
295
|
+
function renderHumanReport(report, dt) {
|
|
294
296
|
writeStdout(`${renderStatus(report.status)} ${paint.ai("fabric doctor")} ${paint.human(report.summary.target)}`);
|
|
295
297
|
for (const check of report.checks) {
|
|
296
298
|
writeStdout(`${renderStatus(check.status)} ${check.name}: ${check.message}`);
|
|
297
299
|
}
|
|
298
|
-
writeIssueSection(
|
|
299
|
-
writeIssueSection(
|
|
300
|
-
writeIssueSection(
|
|
300
|
+
writeIssueSection(dt("doctor.section.fixable"), report.fixable_errors);
|
|
301
|
+
writeIssueSection(dt("doctor.section.manual"), report.manual_errors);
|
|
302
|
+
writeIssueSection(dt("doctor.section.warnings"), report.warnings);
|
|
301
303
|
}
|
|
302
|
-
function renderFixKnowledgeMutations(fixKnowledgeReport) {
|
|
304
|
+
function renderFixKnowledgeMutations(fixKnowledgeReport, dt) {
|
|
303
305
|
if (fixKnowledgeReport.mutations.length === 0) {
|
|
304
306
|
return;
|
|
305
307
|
}
|
|
306
308
|
writeStdout("");
|
|
307
|
-
writeStdout(
|
|
309
|
+
writeStdout(dt("doctor.section.fix-knowledge-mutations"));
|
|
308
310
|
for (const mutation of fixKnowledgeReport.mutations) {
|
|
309
311
|
const marker = mutation.applied ? symbol.ok : symbol.error;
|
|
310
312
|
const errSuffix = mutation.applied || mutation.error === void 0 ? "" : ` (${mutation.error})`;
|
|
@@ -319,6 +321,9 @@ function writeIssueSection(title, issues) {
|
|
|
319
321
|
writeStdout(title);
|
|
320
322
|
for (const issue of issues) {
|
|
321
323
|
writeStdout(`- ${issue.code}: ${issue.message}`);
|
|
324
|
+
if (issue.actionHint !== void 0 && issue.actionHint.length > 0) {
|
|
325
|
+
writeStdout(` \u2192 ${issue.actionHint}`);
|
|
326
|
+
}
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
329
|
function renderStatus(status) {
|
|
@@ -436,35 +441,35 @@ var CITE_COVERAGE_LAYER_FILTERS = /* @__PURE__ */ new Set([
|
|
|
436
441
|
function isValidLayerFilter(input) {
|
|
437
442
|
return CITE_COVERAGE_LAYER_FILTERS.has(input);
|
|
438
443
|
}
|
|
439
|
-
function renderCiteCoverageReport(report, jsonMode) {
|
|
444
|
+
function renderCiteCoverageReport(report, jsonMode, dt) {
|
|
440
445
|
if (jsonMode) {
|
|
441
446
|
writeStdout(JSON.stringify(report, null, 2));
|
|
442
447
|
return;
|
|
443
448
|
}
|
|
444
449
|
if (report.status === "skipped") {
|
|
445
|
-
writeStdout(
|
|
450
|
+
writeStdout(dt("doctor.cite.status.skipped"));
|
|
446
451
|
return;
|
|
447
452
|
}
|
|
448
453
|
const lines = [];
|
|
449
|
-
lines.push(
|
|
454
|
+
lines.push(dt("doctor.section.cite-coverage"));
|
|
450
455
|
lines.push(
|
|
451
|
-
|
|
456
|
+
dt("doctor.cite.header", {
|
|
452
457
|
since: new Date(report.since_ts).toISOString(),
|
|
453
458
|
marker: new Date(report.marker_ts).toISOString()
|
|
454
459
|
})
|
|
455
460
|
);
|
|
456
461
|
if (report.marker_emitted_now) {
|
|
457
|
-
lines.push(
|
|
462
|
+
lines.push(dt("doctor.cite.warning.justActivated"));
|
|
458
463
|
}
|
|
459
464
|
lines.push("");
|
|
460
|
-
lines.push(` ${
|
|
461
|
-
lines.push(` ${
|
|
462
|
-
lines.push(` ${
|
|
463
|
-
lines.push(` ${
|
|
464
|
-
lines.push(` ${
|
|
465
|
+
lines.push(` ${dt("doctor.cite.metric.editsTouched")}: ${report.metrics.edits_touched}`);
|
|
466
|
+
lines.push(` ${dt("doctor.cite.metric.qualifyingCites")}: ${report.metrics.qualifying_cites}`);
|
|
467
|
+
lines.push(` ${dt("doctor.cite.metric.recalledUnverified")}: ${report.metrics.recalled_unverified}`);
|
|
468
|
+
lines.push(` ${dt("doctor.cite.metric.expectedButMissed")}: ${report.metrics.expected_but_missed}`);
|
|
469
|
+
lines.push(` ${dt("doctor.cite.metric.totalTurns")}: ${report.metrics.total_turns}`);
|
|
465
470
|
if (report.per_client !== void 0 && Object.keys(report.per_client).length > 1) {
|
|
466
471
|
lines.push("");
|
|
467
|
-
lines.push(`### ${
|
|
472
|
+
lines.push(`### ${dt("doctor.cite.section.perClient")}`);
|
|
468
473
|
for (const [client, metrics] of Object.entries(report.per_client)) {
|
|
469
474
|
const summary = Object.entries(metrics).map(([k, v]) => `${k}=${v}`).join(" / ");
|
|
470
475
|
lines.push(` ${client}: ${summary}`);
|
|
@@ -472,24 +477,24 @@ function renderCiteCoverageReport(report, jsonMode) {
|
|
|
472
477
|
}
|
|
473
478
|
if (report.dismissed_reason_histogram !== void 0 && Object.keys(report.dismissed_reason_histogram).length > 0) {
|
|
474
479
|
lines.push("");
|
|
475
|
-
lines.push(`### ${
|
|
480
|
+
lines.push(`### ${dt("doctor.cite.section.dismissedReasons")}`);
|
|
476
481
|
for (const [reason, count] of Object.entries(report.dismissed_reason_histogram)) {
|
|
477
|
-
const label =
|
|
482
|
+
const label = dt(`doctor.cite.dismissed.${reason}`);
|
|
478
483
|
lines.push(` ${label}: ${count}`);
|
|
479
484
|
}
|
|
480
485
|
}
|
|
481
486
|
if (report.none_reason_histogram !== void 0 && Object.keys(report.none_reason_histogram).length > 0) {
|
|
482
487
|
lines.push("");
|
|
483
|
-
lines.push(`### ${
|
|
488
|
+
lines.push(`### ${dt("doctor.cite.section.noneReasons")}`);
|
|
484
489
|
for (const [reason, count] of Object.entries(report.none_reason_histogram)) {
|
|
485
|
-
const label =
|
|
490
|
+
const label = dt(`doctor.cite.none.${reason}`);
|
|
486
491
|
lines.push(` ${label}: ${count}`);
|
|
487
492
|
}
|
|
488
493
|
}
|
|
489
|
-
appendContractSection(lines, report);
|
|
494
|
+
appendContractSection(lines, report, dt);
|
|
490
495
|
writeStdout(lines.join("\n"));
|
|
491
496
|
}
|
|
492
|
-
function appendContractSection(lines, report) {
|
|
497
|
+
function appendContractSection(lines, report, dt) {
|
|
493
498
|
const status = report.contract_metrics_status;
|
|
494
499
|
if (status === void 0) {
|
|
495
500
|
return;
|
|
@@ -501,13 +506,13 @@ function appendContractSection(lines, report) {
|
|
|
501
506
|
return;
|
|
502
507
|
}
|
|
503
508
|
lines.push("");
|
|
504
|
-
lines.push(`### ${
|
|
509
|
+
lines.push(`### ${dt("cite-coverage.contract.header")}`);
|
|
505
510
|
if (status === "skipped:bootstrap_drift") {
|
|
506
|
-
lines.push(` ${
|
|
511
|
+
lines.push(` ${dt("cite-coverage.contract.status.skipped_bootstrap_drift")}`);
|
|
507
512
|
return;
|
|
508
513
|
}
|
|
509
514
|
const statusKey = status === "ok" ? "cite-coverage.contract.status.ok" : "cite-coverage.contract.status.awaiting_marker";
|
|
510
|
-
lines.push(` status: ${
|
|
515
|
+
lines.push(` status: ${dt(statusKey)}`);
|
|
511
516
|
if (typeof report.contract_marker_ts === "number" && report.contract_marker_ts > 0) {
|
|
512
517
|
lines.push(` since: ${new Date(report.contract_marker_ts).toISOString()}`);
|
|
513
518
|
}
|
|
@@ -515,14 +520,14 @@ function appendContractSection(lines, report) {
|
|
|
515
520
|
lines.push(` layer filter: ${report.layer_filter}`);
|
|
516
521
|
}
|
|
517
522
|
if (metrics !== void 0) {
|
|
518
|
-
lines.push(` ${
|
|
519
|
-
lines.push(` ${
|
|
520
|
-
lines.push(` ${
|
|
521
|
-
lines.push(` ${
|
|
523
|
+
lines.push(` ${dt("cite-coverage.contract.decisions_cited")}: ${metrics.decisions_cited}`);
|
|
524
|
+
lines.push(` ${dt("cite-coverage.contract.pitfalls_cited")}: ${metrics.pitfalls_cited}`);
|
|
525
|
+
lines.push(` ${dt("cite-coverage.contract.with")}: ${metrics.contract_with}`);
|
|
526
|
+
lines.push(` ${dt("cite-coverage.contract.missing")}: ${metrics.contract_missing}`);
|
|
522
527
|
if (metrics.hard_violated > 0) {
|
|
523
|
-
const layerSuffix = report.layer_filter === "personal" ?
|
|
528
|
+
const layerSuffix = report.layer_filter === "personal" ? dt("cite-coverage.layer.personal_fyi") : dt("cite-coverage.layer.team_review");
|
|
524
529
|
lines.push(
|
|
525
|
-
` ${
|
|
530
|
+
` ${dt("cite-coverage.contract.hard_violated")} ${layerSuffix}: ${metrics.hard_violated}`
|
|
526
531
|
);
|
|
527
532
|
}
|
|
528
533
|
}
|
|
@@ -535,39 +540,39 @@ function appendContractSection(lines, report) {
|
|
|
535
540
|
);
|
|
536
541
|
if (teamKeys.length > 0 || personalKeys.length > 0) {
|
|
537
542
|
lines.push("");
|
|
538
|
-
lines.push(`#### ${
|
|
543
|
+
lines.push(`#### ${dt("cite-coverage.layer.team")} \xD7 ${dt("cite-coverage.layer.personal")}`);
|
|
539
544
|
for (const key of teamKeys) {
|
|
540
|
-
const label =
|
|
541
|
-
lines.push(` ${
|
|
545
|
+
const label = dt(`cite-coverage.contract.type.${key}`);
|
|
546
|
+
lines.push(` ${dt("cite-coverage.layer.team")} \u2014 ${label}: ${perLayerType.team[key]}`);
|
|
542
547
|
}
|
|
543
548
|
for (const key of personalKeys) {
|
|
544
|
-
const label =
|
|
549
|
+
const label = dt(`cite-coverage.contract.type.${key}`);
|
|
545
550
|
lines.push(
|
|
546
|
-
` ${
|
|
551
|
+
` ${dt("cite-coverage.layer.personal")} \u2014 ${label}: ${perLayerType.personal[key]}`
|
|
547
552
|
);
|
|
548
553
|
}
|
|
549
554
|
}
|
|
550
555
|
}
|
|
551
556
|
if (metrics !== void 0 && Object.keys(metrics.skip_count).length > 0) {
|
|
552
557
|
lines.push("");
|
|
553
|
-
lines.push(`#### ${
|
|
558
|
+
lines.push(`#### ${dt("cite-coverage.contract.skip_count")}`);
|
|
554
559
|
for (const [reason, count] of Object.entries(metrics.skip_count)) {
|
|
555
|
-
const label =
|
|
560
|
+
const label = dt(`cite-coverage.skip.${reason}`);
|
|
556
561
|
lines.push(` ${label}: ${count}`);
|
|
557
562
|
}
|
|
558
563
|
}
|
|
559
564
|
if (metrics !== void 0 && metrics.cite_id_unresolved > 0) {
|
|
560
565
|
lines.push("");
|
|
561
566
|
lines.push(
|
|
562
|
-
`${symbol.warn} ${
|
|
567
|
+
`${symbol.warn} ${dt("cite-coverage.contract.cite_id_unresolved")}: ${metrics.cite_id_unresolved}`
|
|
563
568
|
);
|
|
564
569
|
}
|
|
565
570
|
}
|
|
566
|
-
function renderEnrichDescriptionsReport(report) {
|
|
571
|
+
function renderEnrichDescriptionsReport(report, dt) {
|
|
567
572
|
const header = `${symbol.ok} ${paint.ai("fab doctor --enrich-descriptions")} mode=${report.mode}${report.dryRun ? " (dry-run)" : ""} scanned=${report.scanned} modified=${report.modified} skipped=${report.skipped}`;
|
|
568
573
|
writeStdout(header);
|
|
569
574
|
if (report.candidates.length === 0) {
|
|
570
|
-
writeStdout(
|
|
575
|
+
writeStdout(dt("doctor.enrich.allComplete"));
|
|
571
576
|
return;
|
|
572
577
|
}
|
|
573
578
|
writeStdout("");
|
|
@@ -611,14 +616,14 @@ function parseSinceDuration(input) {
|
|
|
611
616
|
}
|
|
612
617
|
throw new Error(`invalid --since value: ${input}`);
|
|
613
618
|
}
|
|
614
|
-
function renderArchiveHistoryReport(report, sinceLabel) {
|
|
619
|
+
function renderArchiveHistoryReport(report, sinceLabel, dt) {
|
|
615
620
|
if (report.entries.length === 0) {
|
|
616
|
-
writeStdout(
|
|
621
|
+
writeStdout(dt("doctor.archive-history.empty", { sinceLabel }));
|
|
617
622
|
return;
|
|
618
623
|
}
|
|
619
624
|
const lines = [];
|
|
620
625
|
lines.push(
|
|
621
|
-
|
|
626
|
+
dt("doctor.archive-history.header", {
|
|
622
627
|
sinceLabel,
|
|
623
628
|
count: String(report.total),
|
|
624
629
|
plural: report.total === 1 ? "" : "s"
|
|
@@ -626,11 +631,11 @@ function renderArchiveHistoryReport(report, sinceLabel) {
|
|
|
626
631
|
);
|
|
627
632
|
lines.push("");
|
|
628
633
|
lines.push(
|
|
629
|
-
`| ${
|
|
634
|
+
`| ${dt("doctor.archive-history.table.session")} | ${dt(
|
|
630
635
|
"doctor.archive-history.table.lastAttempt"
|
|
631
|
-
)} | ${
|
|
636
|
+
)} | ${dt("doctor.archive-history.table.outcome")} | ${dt(
|
|
632
637
|
"doctor.archive-history.table.candidates"
|
|
633
|
-
)} | ${
|
|
638
|
+
)} | ${dt("doctor.archive-history.table.coveredGap")} |`
|
|
634
639
|
);
|
|
635
640
|
lines.push("| ------- | ---------------- | -------- | ---------- | ----------- |");
|
|
636
641
|
for (const entry of report.entries) {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
t
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PWLW3B57.js";
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
import { realpathSync } from "fs";
|
|
@@ -11,12 +11,12 @@ import { defineCommand, runMain } from "citty";
|
|
|
11
11
|
|
|
12
12
|
// src/commands/index.ts
|
|
13
13
|
var allCommands = {
|
|
14
|
-
install: () => import("./install-
|
|
15
|
-
doctor: () => import("./doctor-
|
|
16
|
-
serve: () => import("./serve-
|
|
17
|
-
uninstall: () => import("./uninstall-
|
|
18
|
-
config: () => import("./config-
|
|
19
|
-
"plan-context-hint": () => import("./plan-context-hint-
|
|
14
|
+
install: () => import("./install-UJOFZUYF.js").then((module) => module.default),
|
|
15
|
+
doctor: () => import("./doctor-ZIQXN2T2.js").then((module) => module.default),
|
|
16
|
+
serve: () => import("./serve-U3TPWDOB.js").then((module) => module.default),
|
|
17
|
+
uninstall: () => import("./uninstall-O3PXESM2.js").then((module) => module.default),
|
|
18
|
+
config: () => import("./config-5CH4EJQ2.js").then((module) => module.default),
|
|
19
|
+
"plan-context-hint": () => import("./plan-context-hint-CXTLNVSV.js").then((module) => module.default),
|
|
20
20
|
// v2.0.0-rc.23 TASK-014 (F8c): S5 onboard-slot coverage. Used by the
|
|
21
21
|
// fabric-archive Skill's first-run phase to detect unclaimed slots.
|
|
22
22
|
"onboard-coverage": () => import("./onboard-coverage-JJ5NGU7I.js").then((module) => module.default)
|
|
@@ -26,7 +26,7 @@ var allCommands = {
|
|
|
26
26
|
var main = defineCommand({
|
|
27
27
|
meta: {
|
|
28
28
|
name: "fabric",
|
|
29
|
-
version: "2.0.0-rc.
|
|
29
|
+
version: "2.0.0-rc.27",
|
|
30
30
|
description: t("cli.main.description")
|
|
31
31
|
},
|
|
32
32
|
subCommands: allCommands
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
installMcpClients
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SRX7WZUG.js";
|
|
5
5
|
import {
|
|
6
6
|
installArchiveHintHook,
|
|
7
7
|
installFabricArchiveSkill,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeCodexBootstrapManagedBlock,
|
|
19
19
|
writeCursorBootstrapManagedBlock,
|
|
20
20
|
writeFabricAgentsSnapshot
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-XEGXQOOJ.js";
|
|
22
22
|
import {
|
|
23
23
|
detectClientSupports
|
|
24
24
|
} from "./chunk-MF3OTILQ.js";
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "./chunk-G2CIOLD4.js";
|
|
32
32
|
import {
|
|
33
33
|
t
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-PWLW3B57.js";
|
|
35
35
|
import {
|
|
36
36
|
createDebugLogger,
|
|
37
37
|
resolveDevMode
|
|
@@ -1348,7 +1348,7 @@ function readProjectName(target) {
|
|
|
1348
1348
|
return basename(target);
|
|
1349
1349
|
}
|
|
1350
1350
|
function getCliVersion() {
|
|
1351
|
-
return true ? "2.0.0-rc.
|
|
1351
|
+
return true ? "2.0.0-rc.27" : "unknown";
|
|
1352
1352
|
}
|
|
1353
1353
|
function sortRecord(record) {
|
|
1354
1354
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
@@ -70,14 +70,29 @@ async function runPlanContextHint(opts) {
|
|
|
70
70
|
id: item.stable_id,
|
|
71
71
|
type: item.type ?? item.description.knowledge_type ?? "",
|
|
72
72
|
maturity: item.maturity ?? item.description.maturity ?? "",
|
|
73
|
-
summary: item.description.summary
|
|
73
|
+
summary: item.description.summary,
|
|
74
|
+
// v2.0.0-rc.27 TASK-002 (§2.5/§2.7): forward the server-side scope.
|
|
75
|
+
// RuleDescriptionIndexItem already carries this field — knowledge-meta-
|
|
76
|
+
// builder defaults to "broad" for entries without an explicit
|
|
77
|
+
// relevance_scope frontmatter, so this read is total and never undefined.
|
|
78
|
+
relevance_scope: item.relevance_scope ?? "broad"
|
|
74
79
|
}));
|
|
80
|
+
let narrow_count = 0;
|
|
81
|
+
let broad_only_count = 0;
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
if (e.relevance_scope === "narrow") narrow_count += 1;
|
|
84
|
+
else broad_only_count += 1;
|
|
85
|
+
}
|
|
75
86
|
const output = {
|
|
76
87
|
version: 2,
|
|
77
88
|
revision_hash: result.revision_hash,
|
|
78
89
|
target_paths: targetPaths,
|
|
79
90
|
entries,
|
|
80
|
-
|
|
91
|
+
// Legacy field — preserved for v2 consumers that haven't migrated. Value
|
|
92
|
+
// semantics unchanged from rc.18 (sharedIndex total).
|
|
93
|
+
broad_count: sharedIndex.length,
|
|
94
|
+
narrow_count,
|
|
95
|
+
broad_only_count
|
|
81
96
|
};
|
|
82
97
|
if (result.auto_healed === true) {
|
|
83
98
|
output.auto_healed = true;
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
HOOK_SCRIPT_DESTINATIONS,
|
|
8
8
|
SKILL_DESTINATIONS,
|
|
9
9
|
fabricAgentsSnapshotPath
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-XEGXQOOJ.js";
|
|
11
11
|
import {
|
|
12
12
|
detectClientSupports,
|
|
13
13
|
resolveClients
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "./chunk-G2CIOLD4.js";
|
|
20
20
|
import {
|
|
21
21
|
t
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-PWLW3B57.js";
|
|
23
23
|
import {
|
|
24
24
|
createDebugLogger,
|
|
25
25
|
resolveDevMode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-cli",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fab": "dist/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"tree-sitter-javascript": "^0.25.0",
|
|
21
21
|
"tree-sitter-typescript": "^0.23.2",
|
|
22
22
|
"web-tree-sitter": "^0.26.8",
|
|
23
|
-
"@fenglimg/fabric-server": "2.0.0-rc.
|
|
24
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
23
|
+
"@fenglimg/fabric-server": "2.0.0-rc.27",
|
|
24
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.27"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^22.15.0",
|
|
@@ -477,14 +477,49 @@ function getTopEditedDirectories(projectRoot, topN, anchorTs) {
|
|
|
477
477
|
// any leading "./". POSIX-style only — the hook ships under POSIX
|
|
478
478
|
// path conventions even on Windows (the project doesn't currently
|
|
479
479
|
// ship a CRLF/backslash test matrix for the sidecar).
|
|
480
|
-
|
|
480
|
+
//
|
|
481
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.8 leak surface): absolute paths
|
|
482
|
+
// already accumulated in legacy sidecars start with `/`. We strip
|
|
483
|
+
// the leading slash and also reject buckets that resolve to user-home
|
|
484
|
+
// segments (`Users/<name>/...`, `home/<name>/...`) so historical
|
|
485
|
+
// pollution from absolute-path writes doesn't surface the user's
|
|
486
|
+
// $HOME in the archive banner. The rc.27 appendEditCounter no longer
|
|
487
|
+
// writes such paths, but the sidecar is append-only so old lines
|
|
488
|
+
// persist until rotation.
|
|
489
|
+
let norm = p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
490
|
+
// Strip leading `/` so a stale absolute entry doesn't generate a leak.
|
|
491
|
+
while (norm.startsWith("/")) norm = norm.slice(1);
|
|
481
492
|
const segs = norm.split("/").filter((s) => s.length > 0);
|
|
493
|
+
// Reject any bucket whose top segments look like a host-system home
|
|
494
|
+
// prefix. The pattern is `<top>/<user>/...` where top ∈ Users|home|root.
|
|
495
|
+
// This silently drops legacy absolute-path entries from $HOME without
|
|
496
|
+
// mangling the buckets for legitimate project-relative `Users/...`
|
|
497
|
+
// (unlikely but possible) — the heuristic favours $HOME leak prevention
|
|
498
|
+
// over false-positive bucketing of project paths named after Unix
|
|
499
|
+
// conventions.
|
|
500
|
+
if (segs.length >= 2 && (segs[0] === "Users" || segs[0] === "home" || segs[0] === "root")) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.8 file-as-dir): when segs[1] looks
|
|
504
|
+
// like a file (contains a dot-extension at the end), surface segs[0]
|
|
505
|
+
// alone instead of `segs[0]/segs[1]/` — a 2-seg path of the form
|
|
506
|
+
// `assets/foo.ts` would otherwise render as "assets/foo.ts/" which
|
|
507
|
+
// misleads the operator about whether they're seeing a file or a
|
|
508
|
+
// directory. The extension regex is permissive: any `.X` where X is
|
|
509
|
+
// 1-8 alphanumerics counts. README.md / package.json / foo.ts all
|
|
510
|
+
// match; "v1.2" or "dotted.module" do too — acceptable false-positive
|
|
511
|
+
// rate, since the worst outcome is over-aggregation to the parent.
|
|
512
|
+
const looksLikeFile = (segment) => /\.[A-Za-z0-9]{1,8}$/u.test(segment);
|
|
482
513
|
let bucket;
|
|
483
514
|
if (segs.length >= 2) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
515
|
+
if (looksLikeFile(segs[1])) {
|
|
516
|
+
bucket = `${segs[0]}/`;
|
|
517
|
+
} else {
|
|
518
|
+
// Leading 2 segments: "packages/cli", "docs/decisions", etc. We
|
|
519
|
+
// trail with "/" so the banner reads "packages/cli/" — clearly a
|
|
520
|
+
// directory rather than a file basename.
|
|
521
|
+
bucket = `${segs[0]}/${segs[1]}/`;
|
|
522
|
+
}
|
|
488
523
|
} else if (segs.length === 1) {
|
|
489
524
|
// Single segment — treat the basename as its own bucket. Bare
|
|
490
525
|
// root-level files (README.md, package.json) get some signal too.
|
|
@@ -653,13 +688,25 @@ function decide(events, now, pendingStats, underseedStats, editCounterStats, thr
|
|
|
653
688
|
// - "<editCount> 次编辑"
|
|
654
689
|
// - "阈值 <N>"
|
|
655
690
|
// - "fabric-archive"
|
|
691
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.17): parts now assembled per-variant
|
|
692
|
+
// via banner-i18n's archivePartsHours / archivePartsEdits so en mode
|
|
693
|
+
// gets fully-English fragments instead of mixed-language output. zh-CN
|
|
694
|
+
// / zh-CN-hybrid still render the original substring contract verbatim.
|
|
656
695
|
const parts = [];
|
|
657
696
|
if (triggerByHours) {
|
|
658
|
-
parts.push(
|
|
697
|
+
parts.push(
|
|
698
|
+
renderBanner("archivePartsHours", variant, {
|
|
699
|
+
hoursFixed: hoursElapsed.toFixed(1),
|
|
700
|
+
threshold: archiveHintHours,
|
|
701
|
+
}),
|
|
702
|
+
);
|
|
659
703
|
}
|
|
660
704
|
if (triggerByEdits) {
|
|
661
705
|
parts.push(
|
|
662
|
-
|
|
706
|
+
renderBanner("archivePartsEdits", variant, {
|
|
707
|
+
count: editStats.editsSinceLastProposed,
|
|
708
|
+
threshold: editStats.threshold,
|
|
709
|
+
}),
|
|
663
710
|
);
|
|
664
711
|
}
|
|
665
712
|
// rc.16 TASK-002: 5-banner i18n via lib/banner-i18n.cjs. Substring
|
|
@@ -1236,11 +1283,21 @@ function summarizeTranscript(transcriptPath) {
|
|
|
1236
1283
|
if (envelope === null || typeof envelope !== "object") continue;
|
|
1237
1284
|
envelopeIndex += 1;
|
|
1238
1285
|
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
|
|
1286
|
+
// v2.0.0-rc.27 TASK-009 (audit §2.16): Codex CLI uses a different
|
|
1287
|
+
// envelope shape — { type:"response_item", payload:{ type:"message",
|
|
1288
|
+
// role, content:[{type:"input_text"|"output_text", text}] } } — vs Claude
|
|
1289
|
+
// Code's { type:"user", message:{ role, content } }. Resolve role +
|
|
1290
|
+
// content from whichever shape is present; without this, every Codex
|
|
1291
|
+
// session's digest came out empty (audit §2.16 — fixed here).
|
|
1292
|
+
const role =
|
|
1293
|
+
envelope.role ||
|
|
1294
|
+
(envelope.message && envelope.message.role) ||
|
|
1295
|
+
(envelope.payload && envelope.payload.role);
|
|
1242
1296
|
if (role === "user") {
|
|
1243
|
-
const content =
|
|
1297
|
+
const content =
|
|
1298
|
+
envelope.content ||
|
|
1299
|
+
(envelope.message && envelope.message.content) ||
|
|
1300
|
+
(envelope.payload && envelope.payload.content);
|
|
1244
1301
|
if (typeof content === "string") {
|
|
1245
1302
|
out.user_messages.push(content);
|
|
1246
1303
|
} else if (Array.isArray(content)) {
|
|
@@ -1257,7 +1314,10 @@ function summarizeTranscript(transcriptPath) {
|
|
|
1257
1314
|
// entry per assistant envelope (even when no KB: line) so downstream can
|
|
1258
1315
|
// distinguish "turn observed, no KB" (kb_line_raw=null) from "no turn".
|
|
1259
1316
|
if (role === "assistant") {
|
|
1260
|
-
const content =
|
|
1317
|
+
const content =
|
|
1318
|
+
envelope.content ||
|
|
1319
|
+
(envelope.message && envelope.message.content) ||
|
|
1320
|
+
(envelope.payload && envelope.payload.content);
|
|
1261
1321
|
let firstText = null;
|
|
1262
1322
|
if (typeof content === "string") {
|
|
1263
1323
|
firstText = content;
|
|
@@ -1277,6 +1337,17 @@ function summarizeTranscript(transcriptPath) {
|
|
|
1277
1337
|
// with cite_ids). Sentinel `KB: none` contributes a `cite_tags=["none"]`
|
|
1278
1338
|
// entry but no commitment — matches the parseCiteLine index contract.
|
|
1279
1339
|
let citeCommitments = [];
|
|
1340
|
+
// v2.0.0-rc.27 TASK-009: Codex assistant blocks carry text under
|
|
1341
|
+
// `type:"output_text"` (not `type:"text"`). Fall back when no text-typed
|
|
1342
|
+
// block matched but a typed output_text block exists.
|
|
1343
|
+
if (firstText === null && Array.isArray(content)) {
|
|
1344
|
+
for (const block of content) {
|
|
1345
|
+
if (block && typeof block === "object" && block.type === "output_text" && typeof block.text === "string") {
|
|
1346
|
+
firstText = block.text;
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1280
1351
|
if (typeof firstText === "string" && firstText.length > 0) {
|
|
1281
1352
|
// First non-empty line.
|
|
1282
1353
|
const linesOfText = firstText.split(/\r?\n/);
|
|
@@ -1342,6 +1413,27 @@ function summarizeTranscript(transcriptPath) {
|
|
|
1342
1413
|
}
|
|
1343
1414
|
}
|
|
1344
1415
|
}
|
|
1416
|
+
|
|
1417
|
+
// v2.0.0-rc.27 TASK-009 (audit §2.16): Codex apply_patch path. Codex
|
|
1418
|
+
// emits one response_item envelope per file-edit invocation with payload
|
|
1419
|
+
// shape { type:"custom_tool_call", name:"apply_patch", input:<patch
|
|
1420
|
+
// string> }. The patch body lists target files via `*** Update File:`,
|
|
1421
|
+
// `*** Add File:`, `*** Delete File:` directives — harvest those.
|
|
1422
|
+
if (
|
|
1423
|
+
envelope.type === "response_item" &&
|
|
1424
|
+
envelope.payload &&
|
|
1425
|
+
envelope.payload.type === "custom_tool_call" &&
|
|
1426
|
+
envelope.payload.name === "apply_patch" &&
|
|
1427
|
+
typeof envelope.payload.input === "string"
|
|
1428
|
+
) {
|
|
1429
|
+
const patchInput = envelope.payload.input;
|
|
1430
|
+
const fileDirectiveRe = /^\*\*\*\s+(?:Update|Add|Delete)\s+File:\s+(.+?)\s*$/gm;
|
|
1431
|
+
let m;
|
|
1432
|
+
while ((m = fileDirectiveRe.exec(patchInput)) !== null) {
|
|
1433
|
+
const fp = m[1].trim();
|
|
1434
|
+
if (fp.length > 0) out.edit_paths.push(fp);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1345
1437
|
}
|
|
1346
1438
|
// 1-line title = first non-empty user message (trimmed). Falls back to "".
|
|
1347
1439
|
if (out.user_messages.length > 0) {
|
|
@@ -267,8 +267,34 @@ function appendEditCounter(projectRoot, now, paths) {
|
|
|
267
267
|
mkdirSync(dir, { recursive: true });
|
|
268
268
|
}
|
|
269
269
|
const iso = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
|
|
270
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.8): normalize every path to a
|
|
271
|
+
// project-relative form BEFORE persistence. rc.26 wrote whatever the
|
|
272
|
+
// tool_input handed in — frequently absolute paths like
|
|
273
|
+
// `/Users/wepie/.../foo.ts` — which then leaked into the archive banner's
|
|
274
|
+
// "recent activity centered on: Users/wepie/" prose (the dirname pass
|
|
275
|
+
// stripped the leading `/` but produced a $HOME-prefix surface). The
|
|
276
|
+
// normalize-on-write keeps the sidecar containing only project-internal
|
|
277
|
+
// paths so downstream banner rendering can't accidentally surface
|
|
278
|
+
// host-system paths.
|
|
279
|
+
//
|
|
280
|
+
// Strategy: for each path, attempt path.relative(projectRoot, abs). When
|
|
281
|
+
// the result starts with `..` (path is outside the project tree) we
|
|
282
|
+
// silently drop the entry — out-of-tree edits are not meaningful
|
|
283
|
+
// activity for THIS project's banner. Bare relative paths (already in
|
|
284
|
+
// canonical form) round-trip through relative() unchanged.
|
|
285
|
+
const { isAbsolute: pathIsAbsolute, relative: pathRelative } = require("node:path");
|
|
270
286
|
const pathList = Array.isArray(paths)
|
|
271
|
-
? paths
|
|
287
|
+
? paths
|
|
288
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
289
|
+
.map((p) => {
|
|
290
|
+
if (pathIsAbsolute(p)) {
|
|
291
|
+
const rel = pathRelative(projectRoot, p);
|
|
292
|
+
// path.relative returns `..` segments when p escapes projectRoot.
|
|
293
|
+
return rel.startsWith("..") ? null : rel;
|
|
294
|
+
}
|
|
295
|
+
return p;
|
|
296
|
+
})
|
|
297
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
272
298
|
: [];
|
|
273
299
|
const line = JSON.stringify({ ts: iso, paths: pathList });
|
|
274
300
|
appendFileSync(file, `${line}\n`, "utf8");
|
|
@@ -723,7 +749,20 @@ function main(env, stdio) {
|
|
|
723
749
|
if (cliPayload === null || cliPayload === undefined) return;
|
|
724
750
|
|
|
725
751
|
// Protocol v2 (rc.18 TASK-005): wire field is `entries`, no v1 shim.
|
|
726
|
-
|
|
752
|
+
//
|
|
753
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.5/§2.7): filter to entries whose
|
|
754
|
+
// `relevance_scope === "narrow"` so broad cross-cutting entries do NOT
|
|
755
|
+
// pollute the PreToolUse banner. rc.26 emitted broad + narrow as a
|
|
756
|
+
// single list — every Edit fired a hint even for paths the entry never
|
|
757
|
+
// anchored against (audit §2.5 reproduction). Broad entries are already
|
|
758
|
+
// surfaced once per session by the SessionStart hook so the PreToolUse
|
|
759
|
+
// surface should be narrow-only by design.
|
|
760
|
+
//
|
|
761
|
+
// Defensive default: when the CLI omits `relevance_scope` (older server
|
|
762
|
+
// / malformed item) we treat it as broad and skip — pre-rc.27 entries
|
|
763
|
+
// without the field are exactly the broad-leak surface §2.5 calls out.
|
|
764
|
+
const allEntries = Array.isArray(cliPayload.entries) ? cliPayload.entries : [];
|
|
765
|
+
const narrow = allEntries.filter((entry) => entry && entry.relevance_scope === "narrow");
|
|
727
766
|
if (narrow.length === 0) {
|
|
728
767
|
// rc.6 TASK-023 (E6): silence-counter — matched-narrow == 0. The CLI
|
|
729
768
|
// had a chance to match against the extracted paths but came back
|
|
@@ -116,12 +116,39 @@ const STRINGS = {
|
|
|
116
116
|
// ---- Signal A: archive ----------------------------------------------------
|
|
117
117
|
// Source (zh-CN): fabric-hint.cjs:614 `📋 Fabric: 距上次归档 ${parts}。`
|
|
118
118
|
// params: { parts } where parts is pre-joined `已过 25.0h(阈值 24h)` etc.
|
|
119
|
+
//
|
|
120
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.17): `parts` is now constructed by the
|
|
121
|
+
// sibling archivePartsHours / archivePartsEdits keys (also per-variant) so
|
|
122
|
+
// the caller never hardcodes Chinese into the en banner. The substring
|
|
123
|
+
// contract on "25.0h" / "阈值 N" / "次编辑" is preserved per-variant but
|
|
124
|
+
// each variant gets a coherent monolingual rendering — pre-rc.27 produced
|
|
125
|
+
// mixed-language output like `📋 Fabric: 已过 25.0h since last archive.`
|
|
126
|
+
// (audit §2.17 reproduction).
|
|
119
127
|
archiveLine1: {
|
|
120
128
|
"zh-CN": (p) => `📋 Fabric: 距上次归档 ${p.parts}。`,
|
|
121
129
|
en: (p) => `📋 Fabric: ${p.parts} since last archive.`,
|
|
122
130
|
"zh-CN-hybrid": (p) => `📋 Fabric: 距上次归档 ${p.parts}。`,
|
|
123
131
|
},
|
|
124
132
|
|
|
133
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.17): per-variant assembly of the
|
|
134
|
+
// hours-trigger fragment. zh-CN tightens to the original substring
|
|
135
|
+
// contract (`已过 25.0h(阈值 24h)`); en variant translates the prose
|
|
136
|
+
// while preserving the numeric tokens; hybrid mirrors zh-CN.
|
|
137
|
+
// params: { hoursFixed: string (already toFixed(1)), threshold: number }
|
|
138
|
+
archivePartsHours: {
|
|
139
|
+
"zh-CN": (p) => `已过 ${p.hoursFixed}h(阈值 ${p.threshold}h)`,
|
|
140
|
+
en: (p) => `${p.hoursFixed}h elapsed (threshold ${p.threshold}h)`,
|
|
141
|
+
"zh-CN-hybrid": (p) => `已过 ${p.hoursFixed}h(阈值 ${p.threshold}h)`,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// v2.0.0-rc.27 TASK-005 (audit §2.17): edits-trigger fragment.
|
|
145
|
+
// params: { count: number, threshold: number }
|
|
146
|
+
archivePartsEdits: {
|
|
147
|
+
"zh-CN": (p) => `累计 ${p.count} 次编辑(阈值 ${p.threshold})`,
|
|
148
|
+
en: (p) => `${p.count} edits since last archive (threshold ${p.threshold})`,
|
|
149
|
+
"zh-CN-hybrid": (p) => `累计 ${p.count} 次编辑(阈值 ${p.threshold})`,
|
|
150
|
+
},
|
|
151
|
+
|
|
125
152
|
// Source (zh-CN): fabric-hint.cjs:619 ` 最近活动集中在: ${activity}。`
|
|
126
153
|
// params: { activity }
|
|
127
154
|
archiveActivity: {
|
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
const ID_RE = /^K[TP]-[A-Z]+-\d+$/;
|
|
30
30
|
const SENTINEL_RE = /^KB:\s*none\b\s*(?:\[[^\]]*\])?\s*$/i;
|
|
31
|
+
// v2.0.0-rc.27 TASK-003 (audit §2.18): multi-id citations supported via
|
|
32
|
+
// comma-separated ID group. Mirrors packages/shared/src/cite-line-parser.ts.
|
|
31
33
|
const FULL_RE =
|
|
32
|
-
/^KB:\s+(K[TP]-[A-Z]+-\d+)(?:\s+\(([^)]*)\))?(?:\s+\[([^\]]+)\])?(?:\s+→\s*(.+))?\s*$/;
|
|
34
|
+
/^KB:\s+(K[TP]-[A-Z]+-\d+(?:\s*,\s*K[TP]-[A-Z]+-\d+)*)(?:\s+\(([^)]*)\))?(?:\s+\[([^\]]+)\])?(?:\s+→\s*(.+))?\s*$/;
|
|
35
|
+
const CHAINED_FROM_ID_RE = /chained-from\s+(K[TP]-[A-Z]+-\d+)/i;
|
|
33
36
|
|
|
34
37
|
const ALLOWED_TAGS = new Set([
|
|
35
38
|
"planned",
|
|
@@ -80,15 +83,32 @@ function parseLine(line) {
|
|
|
80
83
|
const trimmed = line.trim();
|
|
81
84
|
if (trimmed.length === 0) return null;
|
|
82
85
|
if (SENTINEL_RE.test(trimmed)) {
|
|
83
|
-
return {
|
|
86
|
+
return { ids: [], tag: "none", commitment: null };
|
|
84
87
|
}
|
|
85
88
|
const fullMatch = trimmed.match(FULL_RE);
|
|
86
89
|
if (fullMatch) {
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
// v2.0.0-rc.27 TASK-003 (audit §2.18): split + revalidate each id;
|
|
91
|
+
// capture chained-from tail id when present.
|
|
92
|
+
const primaryIds = fullMatch[1]
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((part) => part.trim())
|
|
95
|
+
.filter((part) => part.length > 0);
|
|
96
|
+
if (primaryIds.some((id) => !ID_RE.test(id))) return null;
|
|
97
|
+
|
|
98
|
+
const rawTag = fullMatch[3];
|
|
99
|
+
const tag = parseTag(rawTag);
|
|
100
|
+
|
|
101
|
+
const chainedIds = [];
|
|
102
|
+
if (rawTag) {
|
|
103
|
+
const chained = CHAINED_FROM_ID_RE.exec(rawTag);
|
|
104
|
+
if (chained && ID_RE.test(chained[1])) {
|
|
105
|
+
chainedIds.push(chained[1]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
return {
|
|
90
|
-
|
|
91
|
-
tag
|
|
110
|
+
ids: primaryIds.concat(chainedIds),
|
|
111
|
+
tag,
|
|
92
112
|
commitment: parseContractTail(fullMatch[4]),
|
|
93
113
|
};
|
|
94
114
|
}
|
|
@@ -99,6 +119,10 @@ function parseLine(line) {
|
|
|
99
119
|
* Parse one or more newline-separated `KB:` cite lines into structured arrays
|
|
100
120
|
* matching the assistant_turn_observed event-ledger fields. Tolerates
|
|
101
121
|
* whitespace, CR/LF, blank lines, interleaved prose. Never throws.
|
|
122
|
+
*
|
|
123
|
+
* v2.0.0-rc.27 TASK-003 (audit §2.18): supports multi-id citations
|
|
124
|
+
* (`KB: KT-DEC-0001, KT-PIT-0005 ...`) and surfaces `chained-from <id>`'s
|
|
125
|
+
* embedded id as an additional cite_id. cite_tags carries one tag per LINE.
|
|
102
126
|
*/
|
|
103
127
|
function parseCiteLine(raw) {
|
|
104
128
|
const result = { cite_ids: [], cite_tags: [], cite_commitments: [] };
|
|
@@ -107,9 +131,19 @@ function parseCiteLine(raw) {
|
|
|
107
131
|
const parsed = parseLine(line);
|
|
108
132
|
if (!parsed) continue;
|
|
109
133
|
result.cite_tags.push(parsed.tag);
|
|
110
|
-
|
|
134
|
+
for (const id of parsed.ids) {
|
|
135
|
+
result.cite_ids.push(id);
|
|
136
|
+
}
|
|
111
137
|
if (parsed.commitment !== null) {
|
|
112
|
-
|
|
138
|
+
// v2.0.0-rc.27.1 (Codex review fix): cite_commitments MUST be index-
|
|
139
|
+
// aligned with cite_ids per the schema doc on event-ledger.ts:428.
|
|
140
|
+
// Multi-id citations share ONE parsed contract — propagate it across
|
|
141
|
+
// every id slot so downstream consumers (`doctor.ts` per-cite walk +
|
|
142
|
+
// `cite-contract-reminder.cjs`) can look up `commitments[i]` for any
|
|
143
|
+
// valid `i < cite_ids.length` without falling into an undefined slot.
|
|
144
|
+
for (let i = 0; i < parsed.ids.length; i += 1) {
|
|
145
|
+
result.cite_commitments.push(parsed.commitment);
|
|
146
|
+
}
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
return result;
|
|
@@ -269,10 +269,11 @@ following 5 categories MUST be rendered in the resolved language:
|
|
|
269
269
|
3. **Confirmation prompts** — the per-candidate `Confirm? (Y to accept,
|
|
270
270
|
edit … inline, N to skip)` line in the batch review template. zh-CN
|
|
271
271
|
↔ en mirror.
|
|
272
|
-
4. **Dry-run table headers** —
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
4. **Dry-run table headers** — v2.0.0-rc.27 TASK-007 added a dry-run
|
|
273
|
+
override path (see Phase 2.5 "dry-run") so users can preview the
|
|
274
|
+
archive proposal without writing pending entries. The dry-run summary
|
|
275
|
+
header and per-candidate preview labels MUST be bilingualized per
|
|
276
|
+
this policy. zh-CN ↔ en mirror.
|
|
276
277
|
5. **AskUserQuestion** — `header` + `question` fields (NOT `options[]`).
|
|
277
278
|
zh-CN ↔ en mirror. fabric-archive itself does not surface
|
|
278
279
|
AskUserQuestion in the current contract (Phase 1 batch review is a
|
|
@@ -1095,6 +1096,20 @@ If the skill needs to record a genuinely separate observation in the same sessio
|
|
|
1095
1096
|
|
|
1096
1097
|
MANDATORY closing step on every skill invocation — runs AFTER Phase 2 (success path) AND on every early-exit path (Phase 0.0 dropped-all, Phase 0.5 gate-FAIL silent-skip or user-active, Phase 1 batch user-dismissed). Drives the Q3.4 outcome state machine + cross-session digest rescan filter.
|
|
1097
1098
|
|
|
1099
|
+
#### Dry-run override (v2.0.0-rc.27 TASK-007 / audit §2.25)
|
|
1100
|
+
|
|
1101
|
+
When the user's invocation explicitly carries a dry-run intent — the prompt or `/fabric-archive` invocation contains a literal `--dry-run`, `dry-run`, `dry_run`, or `预览` token — the skill MUST skip Phase 2.5's ledger write. The mandatory contract above is suspended only in this single case; every other early-exit path still emits the event.
|
|
1102
|
+
|
|
1103
|
+
Rationale: pre-rc.27 the spec read as "MANDATORY on every invocation" which created an irreconcilable conflict when the user explicitly requested a no-mutation preview (audit §2.25). The dry-run override resolves the deadlock by treating dry-run as an entry-context override that disables the ledger side-effect while preserving the rest of the skill's read-side machinery (Phase 0.0 digest collection, Phase 0.5 viability gate, Phase 1 candidate preview render). The user sees what WOULD have happened without the audit trail recording an attempt that never produced a pending entry.
|
|
1104
|
+
|
|
1105
|
+
Detection rule (substring match, case-insensitive): if the originating prompt contains `--dry-run` | `dry-run` | `dry_run` | `预览` as a standalone token, set `dry_run = true` for the entire skill run and skip the Phase 2.5 event emission. All other phases run normally; their user-facing output should prefix `[DRY-RUN]` to make the mode visible.
|
|
1106
|
+
|
|
1107
|
+
When `dry_run = true`:
|
|
1108
|
+
- Phase 1 batch review header MUST include `[DRY-RUN — no writes will occur]`
|
|
1109
|
+
- Phase 2 candidate emission is REPLACED with a "would write N pending entries" preview rendered as a numbered table (`would-write` shape — same columns as the real Phase 1 review)
|
|
1110
|
+
- Phase 2.5 event emission is SKIPPED entirely (the rationale above)
|
|
1111
|
+
- No `fab_extract_knowledge` MCP call is issued (dry-run is purely read-side)
|
|
1112
|
+
|
|
1098
1113
|
#### What to emit
|
|
1099
1114
|
|
|
1100
1115
|
For EACH `session_id` in the run's scope (multi-session E4 runs emit MULTIPLE events — one per session_id; single-session E1/E2/E3/E5 runs emit ONE event), append ONE `session_archive_attempted` line to `.fabric/events.jsonl`:
|