openhermes 4.12.1 → 4.13.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.
Files changed (73) hide show
  1. package/CONTEXT.md +6 -6
  2. package/ETHOS.md +2 -2
  3. package/README.md +11 -17
  4. package/bootstrap.ts +118 -126
  5. package/docs/HOW-IT-WORKS.md +162 -0
  6. package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
  7. package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
  8. package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
  9. package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
  10. package/docs/adr/ADR-0005-hook-system-design.md +42 -0
  11. package/docs/adr/README.md +9 -0
  12. package/harness/codex/AUTOPILOT.md +35 -40
  13. package/harness/codex/CHARTER.md +3 -3
  14. package/harness/lib/composer/compose.test.ts +29 -29
  15. package/harness/lib/composer/fragments/02-delegation.md +5 -5
  16. package/harness/lib/composer/fragments/04-task-flow.md +13 -13
  17. package/harness/lib/composer/fragments/08-routing.md +1 -1
  18. package/harness/lib/composer/fragments/09-guardrails.md +25 -25
  19. package/harness/lib/composer/index.ts +1 -1
  20. package/harness/lib/guards/guard-config.ts +72 -72
  21. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
  22. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
  23. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
  24. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
  25. package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
  26. package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
  27. package/harness/lib/hooks/hooks.test.ts +160 -324
  28. package/harness/lib/hooks/index.ts +38 -42
  29. package/harness/lib/hooks/registry.ts +309 -416
  30. package/harness/lib/hooks/types.ts +116 -119
  31. package/harness/lib/plans/plan-location.ts +134 -134
  32. package/harness/lib/routing/index.ts +21 -21
  33. package/harness/lib/routing/route-guidance.ts +147 -147
  34. package/harness/lib/routing/route-resolver.ts +58 -58
  35. package/harness/lib/routing/routing.test.ts +195 -195
  36. package/harness/lib/routing/skill-frontmatter.ts +125 -125
  37. package/harness/lib/routing/types.ts +52 -52
  38. package/harness/skills/oh-ascii/SKILL.md +1 -1
  39. package/harness/skills/oh-fusion/DEEP.md +109 -109
  40. package/harness/skills/oh-fusion/SKILL.md +47 -47
  41. package/harness/skills/oh-init/DEEP.md +2 -2
  42. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  43. package/harness/skills/oh-planner/DEEP.md +3 -3
  44. package/harness/skills/oh-review/DEEP.md +5 -5
  45. package/package.json +56 -53
  46. package/harness/lib/background/background.test.ts +0 -216
  47. package/harness/lib/background/index.ts +0 -7
  48. package/harness/lib/background/interfaces.ts +0 -31
  49. package/harness/lib/background/manager.ts +0 -320
  50. package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
  51. package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
  52. package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
  53. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +0 -93
  54. package/harness/lib/memory/index.ts +0 -18
  55. package/harness/lib/memory/interfaces.ts +0 -53
  56. package/harness/lib/memory/memory-manager.ts +0 -205
  57. package/harness/lib/memory/memory.test.ts +0 -485
  58. package/harness/lib/memory/plan-store.ts +0 -346
  59. package/harness/lib/recovery/handler.ts +0 -243
  60. package/harness/lib/recovery/index.ts +0 -14
  61. package/harness/lib/recovery/interfaces.ts +0 -48
  62. package/harness/lib/recovery/patterns.ts +0 -149
  63. package/harness/lib/recovery/recovery.test.ts +0 -312
  64. package/harness/lib/sanity/anomaly-tracker.ts +0 -127
  65. package/harness/lib/sanity/checker.ts +0 -189
  66. package/harness/lib/sanity/index.ts +0 -13
  67. package/harness/lib/sanity/interfaces.ts +0 -24
  68. package/harness/lib/sanity/sanity.test.ts +0 -472
  69. package/harness/lib/sync/file-watcher.ts +0 -175
  70. package/harness/lib/sync/index.ts +0 -11
  71. package/harness/lib/sync/interfaces.ts +0 -27
  72. package/harness/lib/sync/plan-sync.ts +0 -533
  73. package/harness/lib/sync/sync.test.ts +0 -858
@@ -1,195 +1,195 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { consumeRouteGuidance, parseSkillFrontmatter, resolveRoute } from "./index.ts";
4
-
5
- describe("routing frontmatter", () => {
6
- it("parses scalar route values", () => {
7
- const parsed = parseSkillFrontmatter(`---
8
- name: oh-test
9
- route:
10
- pass: oh-builder
11
- fail: oh-review
12
- blocker: surface
13
- ---`);
14
-
15
- assert.ok(parsed);
16
- assert.deepEqual(parsed.route, {
17
- pass: ["oh-builder"],
18
- fail: ["oh-review"],
19
- blocker: ["surface"],
20
- });
21
- });
22
-
23
- it("parses yaml route lists", () => {
24
- const parsed = parseSkillFrontmatter(`---
25
- name: oh-test
26
- route:
27
- pass:
28
- - oh-gauntlet
29
- - oh-ship
30
- fail:
31
- - oh-builder
32
- blocker: surface
33
- ---`);
34
-
35
- assert.ok(parsed);
36
- assert.deepEqual(parsed.route, {
37
- pass: ["oh-gauntlet", "oh-ship"],
38
- fail: ["oh-builder"],
39
- blocker: ["surface"],
40
- });
41
- });
42
-
43
- it("parses inline route arrays", () => {
44
- const parsed = parseSkillFrontmatter(`---
45
- name: oh-test
46
- route:
47
- pass: [oh-gauntlet, oh-ship]
48
- fail: [surface, oh-expert]
49
- blocker: surface
50
- ---`);
51
-
52
- assert.ok(parsed);
53
- assert.deepEqual(parsed.route, {
54
- pass: ["oh-gauntlet", "oh-ship"],
55
- fail: ["surface", "oh-expert"],
56
- blocker: ["surface"],
57
- });
58
- });
59
- });
60
-
61
- describe("resolveRoute", () => {
62
- const route = {
63
- pass: ["oh-gauntlet", "oh-ship"],
64
- fail: ["oh-builder"],
65
- blocker: ["surface"],
66
- };
67
-
68
- it("defaults to the first candidate for an outcome", () => {
69
- const resolved = resolveRoute(route, { outcome: "pass" });
70
- assert.deepEqual(resolved, {
71
- outcome: "pass",
72
- candidates: ["oh-gauntlet", "oh-ship"],
73
- selected: "oh-gauntlet",
74
- reason: 'Selected first declared route for outcome "pass".',
75
- });
76
- });
77
-
78
- it("prefers an evidence target when it matches a candidate", () => {
79
- const resolved = resolveRoute(route, { outcome: "pass", target: "oh-ship" });
80
- assert.deepEqual(resolved, {
81
- outcome: "pass",
82
- candidates: ["oh-gauntlet", "oh-ship"],
83
- selected: "oh-ship",
84
- reason: 'Selected "oh-ship" from output evidence.',
85
- });
86
- });
87
-
88
- it("prefers oh-ship for verified done ship work", () => {
89
- const resolved = resolveRoute(route, {
90
- outcome: "pass",
91
- verification: "verified",
92
- action: "done",
93
- work: "ship",
94
- });
95
-
96
- assert.deepEqual(resolved, {
97
- outcome: "pass",
98
- verification: "verified",
99
- action: "done",
100
- work: "ship",
101
- candidates: ["oh-gauntlet", "oh-ship"],
102
- selected: "oh-ship",
103
- reason: 'Selected "oh-ship" for verified ship-ready work.',
104
- });
105
- });
106
-
107
- it("prefers oh-gauntlet for unverified work", () => {
108
- const resolved = resolveRoute(route, {
109
- outcome: "pass",
110
- verification: "unverified",
111
- action: "done",
112
- work: "ship",
113
- });
114
-
115
- assert.deepEqual(resolved, {
116
- outcome: "pass",
117
- verification: "unverified",
118
- action: "done",
119
- work: "ship",
120
- candidates: ["oh-gauntlet", "oh-ship"],
121
- selected: "oh-gauntlet",
122
- reason: 'Selected "oh-gauntlet" because work is still unverified.',
123
- });
124
- });
125
-
126
- it("prefers oh-gauntlet for verify work", () => {
127
- const resolved = resolveRoute(route, {
128
- outcome: "pass",
129
- verification: "verified",
130
- action: "done",
131
- work: "verify",
132
- });
133
-
134
- assert.deepEqual(resolved, {
135
- outcome: "pass",
136
- verification: "verified",
137
- action: "done",
138
- work: "verify",
139
- candidates: ["oh-gauntlet", "oh-ship"],
140
- selected: "oh-gauntlet",
141
- reason: 'Selected "oh-gauntlet" for verification work.',
142
- });
143
- });
144
-
145
- it("prefers oh-builder for fixable implementation work", () => {
146
- const resolved = resolveRoute({
147
- pass: ["oh-gauntlet", "oh-ship", "oh-builder"],
148
- fail: ["oh-builder"],
149
- blocker: ["surface"],
150
- }, {
151
- outcome: "pass",
152
- verification: "verified",
153
- action: "fixable",
154
- work: "implement",
155
- });
156
-
157
- assert.deepEqual(resolved, {
158
- outcome: "pass",
159
- verification: "verified",
160
- action: "fixable",
161
- work: "implement",
162
- candidates: ["oh-gauntlet", "oh-ship", "oh-builder"],
163
- selected: "oh-builder",
164
- reason: 'Selected "oh-builder" for fixable implementation work.',
165
- });
166
- });
167
- });
168
-
169
- describe("consumeRouteGuidance", () => {
170
- it("promotes a selected route into an explicit next-route instruction", () => {
171
- const consumed = consumeRouteGuidance([
172
- "Review complete",
173
- 'ROUTE_GUIDANCE: {"outcome":"pass","candidates":["oh-gauntlet","oh-ship"],"selected":"oh-ship","reason":"Selected \\\"oh-ship\\\" from output evidence."}',
174
- ].join("\n"));
175
-
176
- assert.equal(consumed.selected, "oh-ship");
177
- assert.match(consumed.output, /NEXT_ROUTE: oh-ship/);
178
- });
179
-
180
- it("leaves output unchanged when no route guidance is present", () => {
181
- const output = "plain output";
182
- const consumed = consumeRouteGuidance(output);
183
-
184
- assert.equal(consumed.selected, null);
185
- assert.equal(consumed.output, output);
186
- });
187
-
188
- it("ignores malformed route guidance safely", () => {
189
- const output = 'Review complete\nROUTE_GUIDANCE: {"selected":42}';
190
- const consumed = consumeRouteGuidance(output);
191
-
192
- assert.equal(consumed.selected, null);
193
- assert.equal(consumed.output, output);
194
- });
195
- });
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { consumeRouteGuidance, parseSkillFrontmatter, resolveRoute } from "./index.ts";
4
+
5
+ describe("routing frontmatter", () => {
6
+ it("parses scalar route values", () => {
7
+ const parsed = parseSkillFrontmatter(`---
8
+ name: oh-test
9
+ route:
10
+ pass: oh-builder
11
+ fail: oh-review
12
+ blocker: surface
13
+ ---`);
14
+
15
+ assert.ok(parsed);
16
+ assert.deepEqual(parsed.route, {
17
+ pass: ["oh-builder"],
18
+ fail: ["oh-review"],
19
+ blocker: ["surface"],
20
+ });
21
+ });
22
+
23
+ it("parses yaml route lists", () => {
24
+ const parsed = parseSkillFrontmatter(`---
25
+ name: oh-test
26
+ route:
27
+ pass:
28
+ - oh-gauntlet
29
+ - oh-ship
30
+ fail:
31
+ - oh-builder
32
+ blocker: surface
33
+ ---`);
34
+
35
+ assert.ok(parsed);
36
+ assert.deepEqual(parsed.route, {
37
+ pass: ["oh-gauntlet", "oh-ship"],
38
+ fail: ["oh-builder"],
39
+ blocker: ["surface"],
40
+ });
41
+ });
42
+
43
+ it("parses inline route arrays", () => {
44
+ const parsed = parseSkillFrontmatter(`---
45
+ name: oh-test
46
+ route:
47
+ pass: [oh-gauntlet, oh-ship]
48
+ fail: [surface, oh-expert]
49
+ blocker: surface
50
+ ---`);
51
+
52
+ assert.ok(parsed);
53
+ assert.deepEqual(parsed.route, {
54
+ pass: ["oh-gauntlet", "oh-ship"],
55
+ fail: ["surface", "oh-expert"],
56
+ blocker: ["surface"],
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("resolveRoute", () => {
62
+ const route = {
63
+ pass: ["oh-gauntlet", "oh-ship"],
64
+ fail: ["oh-builder"],
65
+ blocker: ["surface"],
66
+ };
67
+
68
+ it("defaults to the first candidate for an outcome", () => {
69
+ const resolved = resolveRoute(route, { outcome: "pass" });
70
+ assert.deepEqual(resolved, {
71
+ outcome: "pass",
72
+ candidates: ["oh-gauntlet", "oh-ship"],
73
+ selected: "oh-gauntlet",
74
+ reason: 'Selected first declared route for outcome "pass".',
75
+ });
76
+ });
77
+
78
+ it("prefers an evidence target when it matches a candidate", () => {
79
+ const resolved = resolveRoute(route, { outcome: "pass", target: "oh-ship" });
80
+ assert.deepEqual(resolved, {
81
+ outcome: "pass",
82
+ candidates: ["oh-gauntlet", "oh-ship"],
83
+ selected: "oh-ship",
84
+ reason: 'Selected "oh-ship" from output evidence.',
85
+ });
86
+ });
87
+
88
+ it("prefers oh-ship for verified done ship work", () => {
89
+ const resolved = resolveRoute(route, {
90
+ outcome: "pass",
91
+ verification: "verified",
92
+ action: "done",
93
+ work: "ship",
94
+ });
95
+
96
+ assert.deepEqual(resolved, {
97
+ outcome: "pass",
98
+ verification: "verified",
99
+ action: "done",
100
+ work: "ship",
101
+ candidates: ["oh-gauntlet", "oh-ship"],
102
+ selected: "oh-ship",
103
+ reason: 'Selected "oh-ship" for verified ship-ready work.',
104
+ });
105
+ });
106
+
107
+ it("prefers oh-gauntlet for unverified work", () => {
108
+ const resolved = resolveRoute(route, {
109
+ outcome: "pass",
110
+ verification: "unverified",
111
+ action: "done",
112
+ work: "ship",
113
+ });
114
+
115
+ assert.deepEqual(resolved, {
116
+ outcome: "pass",
117
+ verification: "unverified",
118
+ action: "done",
119
+ work: "ship",
120
+ candidates: ["oh-gauntlet", "oh-ship"],
121
+ selected: "oh-gauntlet",
122
+ reason: 'Selected "oh-gauntlet" because work is still unverified.',
123
+ });
124
+ });
125
+
126
+ it("prefers oh-gauntlet for verify work", () => {
127
+ const resolved = resolveRoute(route, {
128
+ outcome: "pass",
129
+ verification: "verified",
130
+ action: "done",
131
+ work: "verify",
132
+ });
133
+
134
+ assert.deepEqual(resolved, {
135
+ outcome: "pass",
136
+ verification: "verified",
137
+ action: "done",
138
+ work: "verify",
139
+ candidates: ["oh-gauntlet", "oh-ship"],
140
+ selected: "oh-gauntlet",
141
+ reason: 'Selected "oh-gauntlet" for verification work.',
142
+ });
143
+ });
144
+
145
+ it("prefers oh-builder for fixable implementation work", () => {
146
+ const resolved = resolveRoute({
147
+ pass: ["oh-gauntlet", "oh-ship", "oh-builder"],
148
+ fail: ["oh-builder"],
149
+ blocker: ["surface"],
150
+ }, {
151
+ outcome: "pass",
152
+ verification: "verified",
153
+ action: "fixable",
154
+ work: "implement",
155
+ });
156
+
157
+ assert.deepEqual(resolved, {
158
+ outcome: "pass",
159
+ verification: "verified",
160
+ action: "fixable",
161
+ work: "implement",
162
+ candidates: ["oh-gauntlet", "oh-ship", "oh-builder"],
163
+ selected: "oh-builder",
164
+ reason: 'Selected "oh-builder" for fixable implementation work.',
165
+ });
166
+ });
167
+ });
168
+
169
+ describe("consumeRouteGuidance", () => {
170
+ it("promotes a selected route into an explicit next-route instruction", () => {
171
+ const consumed = consumeRouteGuidance([
172
+ "Review complete",
173
+ 'ROUTE_GUIDANCE: {"outcome":"pass","candidates":["oh-gauntlet","oh-ship"],"selected":"oh-ship","reason":"Selected \\\"oh-ship\\\" from output evidence."}',
174
+ ].join("\n"));
175
+
176
+ assert.equal(consumed.selected, "oh-ship");
177
+ assert.match(consumed.output, /NEXT_ROUTE: oh-ship/);
178
+ });
179
+
180
+ it("leaves output unchanged when no route guidance is present", () => {
181
+ const output = "plain output";
182
+ const consumed = consumeRouteGuidance(output);
183
+
184
+ assert.equal(consumed.selected, null);
185
+ assert.equal(consumed.output, output);
186
+ });
187
+
188
+ it("ignores malformed route guidance safely", () => {
189
+ const output = 'Review complete\nROUTE_GUIDANCE: {"selected":42}';
190
+ const consumed = consumeRouteGuidance(output);
191
+
192
+ assert.equal(consumed.selected, null);
193
+ assert.equal(consumed.output, output);
194
+ });
195
+ });
@@ -1,125 +1,125 @@
1
- import fs from "node:fs";
2
- import type { RouteOutcome, SkillRouteMap, SkillRoutingFrontmatter } from "./types.ts";
3
-
4
- const EMPTY_ROUTES: SkillRouteMap = {
5
- pass: [],
6
- fail: [],
7
- blocker: [],
8
- };
9
-
10
- function stripQuotes(value: string): string {
11
- return value.trim().replace(/^['"]|['"]$/g, "");
12
- }
13
-
14
- function parseRouteValue(value: string): string[] {
15
- const trimmed = value.trim();
16
- if (!trimmed) return [];
17
-
18
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
19
- return trimmed
20
- .slice(1, -1)
21
- .split(",")
22
- .map((entry) => stripQuotes(entry))
23
- .filter(Boolean);
24
- }
25
-
26
- return [stripQuotes(trimmed)].filter(Boolean);
27
- }
28
-
29
- function isRouteOutcome(value: string): value is RouteOutcome {
30
- return value === "pass" || value === "fail" || value === "blocker";
31
- }
32
-
33
- export function extractFrontmatter(source: string): string | null {
34
- const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
35
- return match?.[1] ?? null;
36
- }
37
-
38
- export function parseSkillFrontmatter(source: string): SkillRoutingFrontmatter | null {
39
- const rawFrontmatter = extractFrontmatter(source);
40
- if (!rawFrontmatter) return null;
41
-
42
- const route: SkillRouteMap = {
43
- pass: [],
44
- fail: [],
45
- blocker: [],
46
- };
47
-
48
- let name: string | undefined;
49
- let description: string | undefined;
50
- let tier: string | undefined;
51
- let inRouteBlock = false;
52
- let activeRouteKey: RouteOutcome | null = null;
53
-
54
- for (const rawLine of rawFrontmatter.split(/\r?\n/)) {
55
- const line = rawLine.trimEnd();
56
- const trimmed = line.trim();
57
-
58
- if (!trimmed) continue;
59
-
60
- if (/^route:\s*$/.test(trimmed)) {
61
- inRouteBlock = true;
62
- activeRouteKey = null;
63
- continue;
64
- }
65
-
66
- if (inRouteBlock) {
67
- const routeMatch = line.match(/^\s{2,}(pass|fail|blocker):\s*(.*)$/);
68
- if (routeMatch && isRouteOutcome(routeMatch[1])) {
69
- activeRouteKey = routeMatch[1];
70
- route[activeRouteKey].push(...parseRouteValue(routeMatch[2]));
71
- continue;
72
- }
73
-
74
- const listMatch = line.match(/^\s{4,}-\s+(.+)$/);
75
- if (listMatch && activeRouteKey) {
76
- route[activeRouteKey].push(stripQuotes(listMatch[1]));
77
- continue;
78
- }
79
-
80
- if (!/^\s/.test(line)) {
81
- inRouteBlock = false;
82
- activeRouteKey = null;
83
- } else {
84
- activeRouteKey = null;
85
- continue;
86
- }
87
- }
88
-
89
- const fieldMatch = line.match(/^(name|description|tier):\s*(.+)$/);
90
- if (!fieldMatch) continue;
91
-
92
- const value = stripQuotes(fieldMatch[2]);
93
- switch (fieldMatch[1]) {
94
- case "name":
95
- name = value;
96
- break;
97
- case "description":
98
- description = value;
99
- break;
100
- case "tier":
101
- tier = value;
102
- break;
103
- }
104
- }
105
-
106
- return {
107
- name,
108
- description,
109
- tier,
110
- route,
111
- };
112
- }
113
-
114
- export function readSkillFrontmatter(skillFilePath: string): SkillRoutingFrontmatter | null {
115
- if (!fs.existsSync(skillFilePath)) return null;
116
- return parseSkillFrontmatter(fs.readFileSync(skillFilePath, "utf8"));
117
- }
118
-
119
- export function emptySkillRoutes(): SkillRouteMap {
120
- return {
121
- pass: [...EMPTY_ROUTES.pass],
122
- fail: [...EMPTY_ROUTES.fail],
123
- blocker: [...EMPTY_ROUTES.blocker],
124
- };
125
- }
1
+ import fs from "node:fs";
2
+ import type { RouteOutcome, SkillRouteMap, SkillRoutingFrontmatter } from "./types.ts";
3
+
4
+ const EMPTY_ROUTES: SkillRouteMap = {
5
+ pass: [],
6
+ fail: [],
7
+ blocker: [],
8
+ };
9
+
10
+ function stripQuotes(value: string): string {
11
+ return value.trim().replace(/^['"]|['"]$/g, "");
12
+ }
13
+
14
+ function parseRouteValue(value: string): string[] {
15
+ const trimmed = value.trim();
16
+ if (!trimmed) return [];
17
+
18
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
19
+ return trimmed
20
+ .slice(1, -1)
21
+ .split(",")
22
+ .map((entry) => stripQuotes(entry))
23
+ .filter(Boolean);
24
+ }
25
+
26
+ return [stripQuotes(trimmed)].filter(Boolean);
27
+ }
28
+
29
+ function isRouteOutcome(value: string): value is RouteOutcome {
30
+ return value === "pass" || value === "fail" || value === "blocker";
31
+ }
32
+
33
+ export function extractFrontmatter(source: string): string | null {
34
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
35
+ return match?.[1] ?? null;
36
+ }
37
+
38
+ export function parseSkillFrontmatter(source: string): SkillRoutingFrontmatter | null {
39
+ const rawFrontmatter = extractFrontmatter(source);
40
+ if (!rawFrontmatter) return null;
41
+
42
+ const route: SkillRouteMap = {
43
+ pass: [],
44
+ fail: [],
45
+ blocker: [],
46
+ };
47
+
48
+ let name: string | undefined;
49
+ let description: string | undefined;
50
+ let tier: string | undefined;
51
+ let inRouteBlock = false;
52
+ let activeRouteKey: RouteOutcome | null = null;
53
+
54
+ for (const rawLine of rawFrontmatter.split(/\r?\n/)) {
55
+ const line = rawLine.trimEnd();
56
+ const trimmed = line.trim();
57
+
58
+ if (!trimmed) continue;
59
+
60
+ if (/^route:\s*$/.test(trimmed)) {
61
+ inRouteBlock = true;
62
+ activeRouteKey = null;
63
+ continue;
64
+ }
65
+
66
+ if (inRouteBlock) {
67
+ const routeMatch = line.match(/^\s{2,}(pass|fail|blocker):\s*(.*)$/);
68
+ if (routeMatch && isRouteOutcome(routeMatch[1])) {
69
+ activeRouteKey = routeMatch[1];
70
+ route[activeRouteKey].push(...parseRouteValue(routeMatch[2]));
71
+ continue;
72
+ }
73
+
74
+ const listMatch = line.match(/^\s{4,}-\s+(.+)$/);
75
+ if (listMatch && activeRouteKey) {
76
+ route[activeRouteKey].push(stripQuotes(listMatch[1]));
77
+ continue;
78
+ }
79
+
80
+ if (!/^\s/.test(line)) {
81
+ inRouteBlock = false;
82
+ activeRouteKey = null;
83
+ } else {
84
+ activeRouteKey = null;
85
+ continue;
86
+ }
87
+ }
88
+
89
+ const fieldMatch = line.match(/^(name|description|tier):\s*(.+)$/);
90
+ if (!fieldMatch) continue;
91
+
92
+ const value = stripQuotes(fieldMatch[2]);
93
+ switch (fieldMatch[1]) {
94
+ case "name":
95
+ name = value;
96
+ break;
97
+ case "description":
98
+ description = value;
99
+ break;
100
+ case "tier":
101
+ tier = value;
102
+ break;
103
+ }
104
+ }
105
+
106
+ return {
107
+ name,
108
+ description,
109
+ tier,
110
+ route,
111
+ };
112
+ }
113
+
114
+ export function readSkillFrontmatter(skillFilePath: string): SkillRoutingFrontmatter | null {
115
+ if (!fs.existsSync(skillFilePath)) return null;
116
+ return parseSkillFrontmatter(fs.readFileSync(skillFilePath, "utf8"));
117
+ }
118
+
119
+ export function emptySkillRoutes(): SkillRouteMap {
120
+ return {
121
+ pass: [...EMPTY_ROUTES.pass],
122
+ fail: [...EMPTY_ROUTES.fail],
123
+ blocker: [...EMPTY_ROUTES.blocker],
124
+ };
125
+ }