@ctxr/skill-llm-wiki 1.0.1 → 1.0.2
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/SKILL.md +7 -0
- package/guide/cli.md +3 -2
- package/guide/consumers/index.md +106 -0
- package/guide/consumers/quickstart.md +96 -0
- package/guide/consumers/recipes/ci-gate.md +125 -0
- package/guide/consumers/recipes/dated-wiki.md +131 -0
- package/guide/consumers/recipes/format-gate.md +126 -0
- package/guide/consumers/recipes/post-write-heal.md +125 -0
- package/guide/consumers/recipes/skill-absent.md +111 -0
- package/guide/consumers/recipes/subject-wiki.md +110 -0
- package/guide/consumers/recipes/testing.md +149 -0
- package/guide/index.md +9 -0
- package/guide/ux/user-intent.md +5 -4
- package/package.json +6 -2
- package/scripts/cli.mjs +473 -13
- package/scripts/lib/contract.mjs +229 -0
- package/scripts/lib/heal.mjs +162 -0
- package/scripts/lib/init.mjs +210 -0
- package/scripts/lib/json-envelope.mjs +190 -0
- package/scripts/lib/templates.mjs +78 -0
- package/scripts/lib/where.mjs +71 -0
- package/scripts/testkit/assert-frontmatter.mjs +171 -0
- package/scripts/testkit/cli-run.mjs +95 -0
- package/scripts/testkit/make-wiki-fixture.mjs +301 -0
- package/scripts/testkit/stub-skill.mjs +107 -0
- package/templates/adrs.llmwiki.layout.yaml +33 -0
- package/templates/plans.llmwiki.layout.yaml +34 -0
- package/templates/regressions.llmwiki.layout.yaml +34 -0
- package/templates/reports.llmwiki.layout.yaml +33 -0
- package/templates/runbooks.llmwiki.layout.yaml +33 -0
- package/templates/sessions.llmwiki.layout.yaml +34 -0
package/scripts/cli.mjs
CHANGED
|
@@ -122,15 +122,23 @@ function _depPreflightFailMessage(missing) {
|
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// Skip the dep check entirely for --version
|
|
126
|
-
// debugging a broken install can still get
|
|
127
|
-
//
|
|
128
|
-
//
|
|
125
|
+
// Skip the dep check entirely for --version, --help, `contract`, and
|
|
126
|
+
// `where` so an operator debugging a broken install can still get
|
|
127
|
+
// version/usage output, and so consumers can probe the contract
|
|
128
|
+
// before the runtime dependencies are necessarily resolved. Every
|
|
129
|
+
// other invocation (including `--help` placed AFTER another arg,
|
|
130
|
+
// which is a malformed invocation we don't need to coddle) runs the
|
|
131
|
+
// check.
|
|
129
132
|
const _argvDP = process.argv.slice(2);
|
|
130
|
-
const
|
|
131
|
-
_argvDP[0] === "--version" ||
|
|
133
|
+
const _isPreflightExemptDP =
|
|
134
|
+
_argvDP[0] === "--version" ||
|
|
135
|
+
_argvDP[0] === "--help" ||
|
|
136
|
+
_argvDP[0] === "-h" ||
|
|
137
|
+
_argvDP[0] === "contract" ||
|
|
138
|
+
_argvDP[0] === "where" ||
|
|
139
|
+
_argvDP[0] === "testkit-stub";
|
|
132
140
|
|
|
133
|
-
if (!
|
|
141
|
+
if (!_isPreflightExemptDP) {
|
|
134
142
|
let _missingDP = _depPreflightCheck();
|
|
135
143
|
if (_missingDP.length > 0) {
|
|
136
144
|
process.stderr.write(_depPreflightFailMessage(_missingDP));
|
|
@@ -288,6 +296,18 @@ Remote mirroring (explicit user-invoked only, never auto-pushes):
|
|
|
288
296
|
remote <wiki> list List configured remotes
|
|
289
297
|
sync <wiki> [--remote <name>] Fetch + push tag refs explicitly
|
|
290
298
|
|
|
299
|
+
Consumer-facing scaffolding:
|
|
300
|
+
init <topic> --kind dated|subject [--template <name>] [--force] [--json]
|
|
301
|
+
Seed a topic directory with a shipped
|
|
302
|
+
layout contract. Prints the exact build
|
|
303
|
+
command to run next. Replaces the
|
|
304
|
+
cp + edit + build-flag dance.
|
|
305
|
+
heal <wiki> [--json] Classify validate findings into
|
|
306
|
+
ok / fixable / needs-rebuild / broken
|
|
307
|
+
and name the next command to run.
|
|
308
|
+
Routes consumers through the right
|
|
309
|
+
mutating op without guessing.
|
|
310
|
+
|
|
291
311
|
Low-level script helpers (deterministic, called by Claude):
|
|
292
312
|
ingest <source> Walk source, emit candidate JSON
|
|
293
313
|
draft-leaf <candidate-file> Script-first frontmatter draft for one candidate
|
|
@@ -312,12 +332,30 @@ Tiered-AI flags:
|
|
|
312
332
|
|
|
313
333
|
UX flags:
|
|
314
334
|
--no-prompt Never prompt; fail loud on ambiguity
|
|
315
|
-
--json
|
|
335
|
+
--json Emit JSON on stdout. Operational
|
|
336
|
+
commands (validate, init, heal, rollback)
|
|
337
|
+
use the envelope schema
|
|
338
|
+
skill-llm-wiki/v1 for both success and
|
|
339
|
+
usage-error paths. Probe commands
|
|
340
|
+
(contract, where) emit their own
|
|
341
|
+
command-specific schemas
|
|
342
|
+
(skill-llm-wiki/contract/v1 and
|
|
343
|
+
skill-llm-wiki/where/v1).
|
|
344
|
+
--json-errors Legacy alias for --json, kept for
|
|
345
|
+
backwards compatibility.
|
|
316
346
|
--accept-dirty Operate on a dirty user git repo
|
|
317
347
|
|
|
318
348
|
Rollback flags:
|
|
319
349
|
--to <ref> genesis | <op-id> | pre-<op-id> | HEAD~N
|
|
320
350
|
|
|
351
|
+
Consumer probes (exempt from runtime-dep preflight):
|
|
352
|
+
contract [--json] Print machine-readable format + CLI surface
|
|
353
|
+
contract. Consumers gate on format_version
|
|
354
|
+
instead of drift-testing SKILL.md.
|
|
355
|
+
where [--json] Print absolute paths to the skill root,
|
|
356
|
+
SKILL.md, guide/, templates/, and testkit/.
|
|
357
|
+
Resolves the install path without kit lookup.
|
|
358
|
+
|
|
321
359
|
Global:
|
|
322
360
|
--version Print CLI version
|
|
323
361
|
--help, -h Show this help
|
|
@@ -343,6 +381,7 @@ const FLAG_WITH_VALUE = new Set([
|
|
|
343
381
|
]);
|
|
344
382
|
const FLAG_BOOLEAN = new Set([
|
|
345
383
|
"--no-prompt",
|
|
384
|
+
"--json",
|
|
346
385
|
"--json-errors",
|
|
347
386
|
"--accept-dirty",
|
|
348
387
|
"--accept-foreign-target",
|
|
@@ -437,6 +476,74 @@ async function main() {
|
|
|
437
476
|
return;
|
|
438
477
|
}
|
|
439
478
|
|
|
479
|
+
// `contract` and `where` are exempt from the dep preflight (see
|
|
480
|
+
// the preflight-skip list above) so consumers can probe the
|
|
481
|
+
// skill's surface before the runtime deps are resolvable. Both
|
|
482
|
+
// pull only pure-data from scripts/lib/* and do not touch any
|
|
483
|
+
// wiki state.
|
|
484
|
+
if (argv[0] === "contract") {
|
|
485
|
+
const { getContract, renderContractText } = await import("./lib/contract.mjs");
|
|
486
|
+
const { hasJsonFlag } = await import("./lib/json-envelope.mjs");
|
|
487
|
+
const wantJson = hasJsonFlag(argv.slice(1));
|
|
488
|
+
const contract = getContract();
|
|
489
|
+
if (wantJson) {
|
|
490
|
+
process.stdout.write(JSON.stringify(contract, null, 2) + "\n");
|
|
491
|
+
} else {
|
|
492
|
+
process.stdout.write(renderContractText(contract));
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (argv[0] === "where") {
|
|
497
|
+
const { getWhere, renderWhereText } = await import("./lib/where.mjs");
|
|
498
|
+
const { hasJsonFlag } = await import("./lib/json-envelope.mjs");
|
|
499
|
+
const wantJson = hasJsonFlag(argv.slice(1));
|
|
500
|
+
const info = getWhere();
|
|
501
|
+
if (wantJson) {
|
|
502
|
+
process.stdout.write(JSON.stringify(info, null, 2) + "\n");
|
|
503
|
+
} else {
|
|
504
|
+
process.stdout.write(renderWhereText(info));
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (argv[0] === "testkit-stub") {
|
|
509
|
+
// Shell shorthand for scripts/testkit/stub-skill.mjs. Consumers
|
|
510
|
+
// whose test suites shell out (e.g. zero-deps agent bundles that
|
|
511
|
+
// can't import from node_modules) use this to seed a stub skill
|
|
512
|
+
// install under an arbitrary base directory. Exempt from the
|
|
513
|
+
// runtime-dep preflight because testkit helpers must work in
|
|
514
|
+
// test environments that deliberately lack the runtime deps.
|
|
515
|
+
const rest = argv.slice(1);
|
|
516
|
+
let atDir = null;
|
|
517
|
+
let layout = "claude-skills";
|
|
518
|
+
for (let i = 0; i < rest.length; i++) {
|
|
519
|
+
const tok = rest[i];
|
|
520
|
+
if (tok === "--at") {
|
|
521
|
+
atDir = rest[++i];
|
|
522
|
+
} else if (tok.startsWith("--at=")) {
|
|
523
|
+
atDir = tok.slice("--at=".length);
|
|
524
|
+
} else if (tok === "--layout") {
|
|
525
|
+
layout = rest[++i];
|
|
526
|
+
} else if (tok.startsWith("--layout=")) {
|
|
527
|
+
layout = tok.slice("--layout=".length);
|
|
528
|
+
} else {
|
|
529
|
+
process.stderr.write(
|
|
530
|
+
`testkit-stub: unknown argument "${tok}"\n`,
|
|
531
|
+
);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!atDir) {
|
|
536
|
+
process.stderr.write(
|
|
537
|
+
"testkit-stub: --at <dir> is required (base directory to seed)\n",
|
|
538
|
+
);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
const { stubSkill } = await import("./testkit/stub-skill.mjs");
|
|
542
|
+
const r = await stubSkill({ home: atDir, layout });
|
|
543
|
+
process.stdout.write(`${r.skillMd}\n`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
440
547
|
// The dependency preflight has already run in the pre-import block
|
|
441
548
|
// at the top of this file. By the time we reach this point, every
|
|
442
549
|
// required runtime dep has been verified or the process has exited
|
|
@@ -587,7 +694,8 @@ async function main() {
|
|
|
587
694
|
if (INTENT_SUBCOMMANDS.has(cmd)) {
|
|
588
695
|
const parsed = parseSubArgv(args);
|
|
589
696
|
if (parsed.error) {
|
|
590
|
-
const
|
|
697
|
+
const { hasJsonFlag } = await import("./lib/json-envelope.mjs");
|
|
698
|
+
const jsonMode = hasJsonFlag(args);
|
|
591
699
|
emitIntentError(
|
|
592
700
|
{
|
|
593
701
|
code: "INT-11",
|
|
@@ -599,7 +707,9 @@ async function main() {
|
|
|
599
707
|
);
|
|
600
708
|
}
|
|
601
709
|
const { positionals, flags } = parsed;
|
|
602
|
-
|
|
710
|
+
// `--json` is the canonical flag; `--json-errors` is the legacy
|
|
711
|
+
// alias kept for existing consumers. Either enables JSON output.
|
|
712
|
+
const jsonMode = Boolean(flags.json_errors) || Boolean(flags.json);
|
|
603
713
|
|
|
604
714
|
// `migrate` has its own resolution path — the intent resolver would
|
|
605
715
|
// reject the legacy folder shape as ambiguous.
|
|
@@ -667,6 +777,29 @@ async function main() {
|
|
|
667
777
|
|
|
668
778
|
if (cmd === "rollback") {
|
|
669
779
|
const result = rollbackOperation(plan.target, flags.to);
|
|
780
|
+
if (jsonMode) {
|
|
781
|
+
const { makeEnvelope, writeEnvelope } = await import(
|
|
782
|
+
"./lib/json-envelope.mjs"
|
|
783
|
+
);
|
|
784
|
+
writeEnvelope(
|
|
785
|
+
makeEnvelope({
|
|
786
|
+
command: "rollback",
|
|
787
|
+
target: plan.target,
|
|
788
|
+
verdict: "ok",
|
|
789
|
+
exit: 0,
|
|
790
|
+
diagnostics: [
|
|
791
|
+
{
|
|
792
|
+
code: "ROLLBACK-01",
|
|
793
|
+
severity: "info",
|
|
794
|
+
path: plan.target,
|
|
795
|
+
message: `rolled back to ${result.ref} (sha=${result.sha ?? "n/a"})`,
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
artifacts: { modified: [plan.target] },
|
|
799
|
+
}),
|
|
800
|
+
);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
670
803
|
process.stdout.write(
|
|
671
804
|
`rolled back ${plan.target} to ${result.ref} (${result.sha ?? "n/a"})\n`,
|
|
672
805
|
);
|
|
@@ -815,10 +948,59 @@ async function main() {
|
|
|
815
948
|
break;
|
|
816
949
|
}
|
|
817
950
|
case "validate": {
|
|
818
|
-
|
|
819
|
-
|
|
951
|
+
// Parse flags so that `validate --json <wiki>` works as well
|
|
952
|
+
// as `validate <wiki> --json`, and unknown flags are rejected
|
|
953
|
+
// loudly. Pre-scan --json first so usage errors emitted below
|
|
954
|
+
// respect the JSON contract.
|
|
955
|
+
const { hasJsonFlag } = await import("./lib/json-envelope.mjs");
|
|
956
|
+
const wantJson = hasJsonFlag(args);
|
|
957
|
+
const positionals = [];
|
|
958
|
+
for (const tok of args) {
|
|
959
|
+
if (tok === "--json" || tok === "--json-errors") continue;
|
|
960
|
+
if (typeof tok === "string" && tok.startsWith("--")) {
|
|
961
|
+
await emitConsumerUsageError(
|
|
962
|
+
"validate",
|
|
963
|
+
"VALIDATE-USAGE",
|
|
964
|
+
`validate does not support option ${tok}`,
|
|
965
|
+
wantJson,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
positionals.push(tok);
|
|
969
|
+
}
|
|
970
|
+
if (positionals.length < 1)
|
|
971
|
+
await emitConsumerUsageError(
|
|
972
|
+
"validate",
|
|
973
|
+
"VALIDATE-USAGE",
|
|
974
|
+
"validate requires <wiki>",
|
|
975
|
+
wantJson,
|
|
976
|
+
);
|
|
977
|
+
if (positionals.length > 1)
|
|
978
|
+
await emitConsumerUsageError(
|
|
979
|
+
"validate",
|
|
980
|
+
"VALIDATE-USAGE",
|
|
981
|
+
"validate accepts exactly one <wiki>",
|
|
982
|
+
wantJson,
|
|
983
|
+
);
|
|
984
|
+
const wiki = resolve(positionals[0]);
|
|
985
|
+
const startMs = Date.now();
|
|
820
986
|
const findings = validateWiki(wiki);
|
|
821
987
|
const summary = summariseFindings(findings);
|
|
988
|
+
const exit = summary.errors > 0 ? 2 : 0;
|
|
989
|
+
if (wantJson) {
|
|
990
|
+
const { findingToDiagnostic, makeEnvelope, writeEnvelope } =
|
|
991
|
+
await import("./lib/json-envelope.mjs");
|
|
992
|
+
writeEnvelope(
|
|
993
|
+
makeEnvelope({
|
|
994
|
+
command: "validate",
|
|
995
|
+
target: wiki,
|
|
996
|
+
verdict: exit === 0 ? "ok" : "broken",
|
|
997
|
+
exit,
|
|
998
|
+
diagnostics: findings.map(findingToDiagnostic),
|
|
999
|
+
timing_ms: Date.now() - startMs,
|
|
1000
|
+
}),
|
|
1001
|
+
);
|
|
1002
|
+
process.exit(exit);
|
|
1003
|
+
}
|
|
822
1004
|
for (const f of findings) {
|
|
823
1005
|
const tag =
|
|
824
1006
|
f.severity === "error"
|
|
@@ -832,7 +1014,7 @@ async function main() {
|
|
|
832
1014
|
console.log(
|
|
833
1015
|
`\n${summary.errors} error(s), ${summary.warnings} warning(s)`,
|
|
834
1016
|
);
|
|
835
|
-
process.exit(
|
|
1017
|
+
process.exit(exit);
|
|
836
1018
|
break;
|
|
837
1019
|
}
|
|
838
1020
|
case "shape-check": {
|
|
@@ -875,12 +1057,290 @@ async function main() {
|
|
|
875
1057
|
process.stdout.write(`current → ${args[1]}\n`);
|
|
876
1058
|
break;
|
|
877
1059
|
}
|
|
1060
|
+
case "init": {
|
|
1061
|
+
// `init` seeds a topic directory with a shipped layout contract
|
|
1062
|
+
// so consumers can start building hosted wikis without the
|
|
1063
|
+
// cp-then-build dance. No orchestrator invocation here — that
|
|
1064
|
+
// remains the consumer's explicit `build` call so error paths
|
|
1065
|
+
// stay where they already are.
|
|
1066
|
+
await cmdInit(args);
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
case "heal": {
|
|
1070
|
+
// `heal` classifies validate findings into one of ok, fixable,
|
|
1071
|
+
// needs-rebuild, broken and names the next command to run.
|
|
1072
|
+
// Mutating action stays the consumer's explicit call so the
|
|
1073
|
+
// orchestrator error surface does not leak into this
|
|
1074
|
+
// classify-only path.
|
|
1075
|
+
await cmdHeal(args);
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
878
1078
|
default:
|
|
879
1079
|
printUsage();
|
|
880
1080
|
process.exit(1);
|
|
881
1081
|
}
|
|
882
1082
|
}
|
|
883
1083
|
|
|
1084
|
+
async function cmdInit(args) {
|
|
1085
|
+
const { runInit, InitError, renderInitText } = await import("./lib/init.mjs");
|
|
1086
|
+
const {
|
|
1087
|
+
hasJsonFlag,
|
|
1088
|
+
makeEnvelope,
|
|
1089
|
+
makeErrorEnvelope,
|
|
1090
|
+
writeEnvelope,
|
|
1091
|
+
} = await import("./lib/json-envelope.mjs");
|
|
1092
|
+
const wantJson = hasJsonFlag(args);
|
|
1093
|
+
const fail = (code, message, topic = null) =>
|
|
1094
|
+
initError(code, message, wantJson, topic, {
|
|
1095
|
+
makeErrorEnvelope,
|
|
1096
|
+
writeEnvelope,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// Minimal flag parse for init. Positional is <topic>; flags are
|
|
1100
|
+
// --kind <dated|subject>, --template <name>, --force, --json.
|
|
1101
|
+
let topic = null;
|
|
1102
|
+
let kind = null;
|
|
1103
|
+
let template = null;
|
|
1104
|
+
let force = false;
|
|
1105
|
+
for (let i = 0; i < args.length; i++) {
|
|
1106
|
+
const tok = args[i];
|
|
1107
|
+
if (tok === "--force") {
|
|
1108
|
+
force = true;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (tok === "--json" || tok === "--json-errors") {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (tok === "--kind") {
|
|
1115
|
+
kind = args[++i];
|
|
1116
|
+
if (kind === undefined || kind === "" || kind.startsWith("--")) {
|
|
1117
|
+
fail("INIT-00", `init: flag "--kind" requires a value`);
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (tok.startsWith("--kind=")) {
|
|
1122
|
+
kind = tok.slice("--kind=".length);
|
|
1123
|
+
if (kind === "") {
|
|
1124
|
+
fail("INIT-00", `init: flag "--kind" requires a value`);
|
|
1125
|
+
}
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (tok === "--template") {
|
|
1129
|
+
template = args[++i];
|
|
1130
|
+
if (
|
|
1131
|
+
template === undefined ||
|
|
1132
|
+
template === "" ||
|
|
1133
|
+
template.startsWith("--")
|
|
1134
|
+
) {
|
|
1135
|
+
fail("INIT-00", `init: flag "--template" requires a value`);
|
|
1136
|
+
}
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (tok.startsWith("--template=")) {
|
|
1140
|
+
template = tok.slice("--template=".length);
|
|
1141
|
+
if (template === "") {
|
|
1142
|
+
fail("INIT-00", `init: flag "--template" requires a value`);
|
|
1143
|
+
}
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (tok.startsWith("--")) {
|
|
1147
|
+
fail("INIT-00", `init: unknown flag "${tok}"`);
|
|
1148
|
+
}
|
|
1149
|
+
if (topic === null) {
|
|
1150
|
+
topic = tok;
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
fail("INIT-00", `init: unexpected positional "${tok}"`);
|
|
1154
|
+
}
|
|
1155
|
+
if (!topic) {
|
|
1156
|
+
fail("INIT-01", "init requires a <topic> path");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const startMs = Date.now();
|
|
1160
|
+
let result;
|
|
1161
|
+
try {
|
|
1162
|
+
result = runInit({ topic, kind, template, force, cwd: process.cwd() });
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
if (err instanceof InitError) {
|
|
1165
|
+
fail(err.code, err.message);
|
|
1166
|
+
}
|
|
1167
|
+
throw err;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (wantJson) {
|
|
1171
|
+
writeEnvelope(
|
|
1172
|
+
makeEnvelope({
|
|
1173
|
+
command: "init",
|
|
1174
|
+
target: result.topic,
|
|
1175
|
+
verdict: "initialised",
|
|
1176
|
+
exit: 0,
|
|
1177
|
+
diagnostics: [
|
|
1178
|
+
{
|
|
1179
|
+
code: "NEXT-01",
|
|
1180
|
+
severity: "info",
|
|
1181
|
+
path: result.topic,
|
|
1182
|
+
message:
|
|
1183
|
+
`contract seeded; next step: ` +
|
|
1184
|
+
result.build_command.join(" "),
|
|
1185
|
+
},
|
|
1186
|
+
],
|
|
1187
|
+
artifacts: { created: [result.contract_path] },
|
|
1188
|
+
next: result.next,
|
|
1189
|
+
timing_ms: Date.now() - startMs,
|
|
1190
|
+
}),
|
|
1191
|
+
);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
process.stdout.write(renderInitText(result));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async function cmdHeal(args) {
|
|
1198
|
+
const { runHeal, renderHealText } = await import("./lib/heal.mjs");
|
|
1199
|
+
const { findingToDiagnostic, hasJsonFlag, makeEnvelope, writeEnvelope } =
|
|
1200
|
+
await import("./lib/json-envelope.mjs");
|
|
1201
|
+
|
|
1202
|
+
const wantJson = hasJsonFlag(args);
|
|
1203
|
+
let wikiPath = null;
|
|
1204
|
+
// `--dry-run` is accepted for contract compatibility. heal is
|
|
1205
|
+
// already classify-only (never mutates) so --dry-run is a no-op
|
|
1206
|
+
// label today; the flag will become meaningful once a follow-up
|
|
1207
|
+
// adds --apply for inline fix/rebuild routing.
|
|
1208
|
+
for (const tok of args) {
|
|
1209
|
+
if (tok === "--json" || tok === "--json-errors") continue;
|
|
1210
|
+
if (tok === "--dry-run") continue;
|
|
1211
|
+
if (tok.startsWith("--")) {
|
|
1212
|
+
await emitConsumerUsageError(
|
|
1213
|
+
"heal",
|
|
1214
|
+
"HEAL-USAGE",
|
|
1215
|
+
`heal: unknown flag "${tok}"`,
|
|
1216
|
+
wantJson,
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
if (wikiPath === null) {
|
|
1220
|
+
wikiPath = tok;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
await emitConsumerUsageError(
|
|
1224
|
+
"heal",
|
|
1225
|
+
"HEAL-USAGE",
|
|
1226
|
+
`heal: unexpected positional "${tok}"`,
|
|
1227
|
+
wantJson,
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (!wikiPath) {
|
|
1231
|
+
await emitConsumerUsageError(
|
|
1232
|
+
"heal",
|
|
1233
|
+
"HEAL-USAGE",
|
|
1234
|
+
"heal requires <wiki> as its first argument",
|
|
1235
|
+
wantJson,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const absWiki = resolve(wikiPath);
|
|
1240
|
+
const startMs = Date.now();
|
|
1241
|
+
const result = runHeal(absWiki);
|
|
1242
|
+
|
|
1243
|
+
const diagnostics = result.findings.map(findingToDiagnostic);
|
|
1244
|
+
if (result.next_command) {
|
|
1245
|
+
diagnostics.push({
|
|
1246
|
+
code: "NEXT-01",
|
|
1247
|
+
severity: "info",
|
|
1248
|
+
path: result.target,
|
|
1249
|
+
message: `next: ${result.next_command.join(" ")}`,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
if (result.error) {
|
|
1253
|
+
diagnostics.unshift({
|
|
1254
|
+
code: "HEAL-00",
|
|
1255
|
+
severity: "error",
|
|
1256
|
+
path: result.target,
|
|
1257
|
+
message: result.error,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// heal is advisory, not a validator. Successful classification is
|
|
1262
|
+
// exit 0 regardless of verdict (ok / fixable / needs-rebuild) — the
|
|
1263
|
+
// envelope's `verdict` carries the state. Only a genuinely broken
|
|
1264
|
+
// or unclassifiable wiki is exit 6 (matches the documented "wiki
|
|
1265
|
+
// corrupt" meaning). Consumers gate on the envelope, not exit 2.
|
|
1266
|
+
const exit =
|
|
1267
|
+
result.verdict === "broken" || result.verdict === "ambiguous" ? 6 : 0;
|
|
1268
|
+
const nextField = result.next_command
|
|
1269
|
+
? {
|
|
1270
|
+
command: result.next_command[0],
|
|
1271
|
+
args: result.next_command.slice(1),
|
|
1272
|
+
}
|
|
1273
|
+
: null;
|
|
1274
|
+
if (wantJson) {
|
|
1275
|
+
writeEnvelope(
|
|
1276
|
+
makeEnvelope({
|
|
1277
|
+
command: "heal",
|
|
1278
|
+
target: result.target,
|
|
1279
|
+
verdict: result.verdict,
|
|
1280
|
+
exit,
|
|
1281
|
+
diagnostics,
|
|
1282
|
+
next: nextField,
|
|
1283
|
+
timing_ms: Date.now() - startMs,
|
|
1284
|
+
}),
|
|
1285
|
+
);
|
|
1286
|
+
process.exit(exit);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
process.stdout.write(renderHealText(result));
|
|
1290
|
+
process.exit(exit);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Shared usage-error emitter for operational subcommands that
|
|
1294
|
+
// need to honour --json on the error path as well as success.
|
|
1295
|
+
// Emits the envelope (verdict="ambiguous", exit=1 for usage)
|
|
1296
|
+
// when wantJson, otherwise a plain-text stderr line + exit.
|
|
1297
|
+
// Used by validate, heal, and any future consumer-facing op
|
|
1298
|
+
// whose usage errors must stay parseable.
|
|
1299
|
+
async function emitConsumerUsageError(command, code, message, wantJson) {
|
|
1300
|
+
if (wantJson) {
|
|
1301
|
+
const { makeErrorEnvelope, writeEnvelope } = await import(
|
|
1302
|
+
"./lib/json-envelope.mjs"
|
|
1303
|
+
);
|
|
1304
|
+
writeEnvelope(
|
|
1305
|
+
makeErrorEnvelope({ command, code, message, exit: 1 }),
|
|
1306
|
+
);
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
process.stderr.write(`error: ${message}\n`);
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Map init error codes to the skill's documented exit scheme.
|
|
1314
|
+
// INIT-00 / INIT-01 are CLI usage errors (missing flag, unknown
|
|
1315
|
+
// flag) → exit 1. INIT-02..08 are validation / ambiguity conditions
|
|
1316
|
+
// (bad --kind, template mismatch, contract collision, topic-as-
|
|
1317
|
+
// file, symlink refusal) → exit 2, matching how build/extend etc.
|
|
1318
|
+
// surface validation failures. INIT-08 specifically covers the
|
|
1319
|
+
// symlink-guard path (scripts/lib/init.mjs): the input path exists
|
|
1320
|
+
// but is a symbolic link we refuse to write through; consistent
|
|
1321
|
+
// with INIT-06 ("exists but not a directory") in both severity and
|
|
1322
|
+
// consumer recovery (fix the path, retry).
|
|
1323
|
+
const INIT_USAGE_CODES = new Set(["INIT-00", "INIT-01"]);
|
|
1324
|
+
|
|
1325
|
+
function initError(code, message, wantJson, topic, envelopeHelpers) {
|
|
1326
|
+
const exit = INIT_USAGE_CODES.has(code) ? 1 : 2;
|
|
1327
|
+
if (wantJson && envelopeHelpers) {
|
|
1328
|
+
const { makeErrorEnvelope, writeEnvelope } = envelopeHelpers;
|
|
1329
|
+
writeEnvelope(
|
|
1330
|
+
makeErrorEnvelope({
|
|
1331
|
+
command: "init",
|
|
1332
|
+
code,
|
|
1333
|
+
message,
|
|
1334
|
+
target: topic,
|
|
1335
|
+
exit,
|
|
1336
|
+
}),
|
|
1337
|
+
);
|
|
1338
|
+
process.exit(exit);
|
|
1339
|
+
}
|
|
1340
|
+
process.stderr.write(`error: ${code} ${message}\n`);
|
|
1341
|
+
process.exit(exit);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
884
1344
|
function usageError(msg) {
|
|
885
1345
|
process.stderr.write(`error: ${msg}\n`);
|
|
886
1346
|
printUsage(process.stderr);
|