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.
- package/CONTEXT.md +6 -6
- package/ETHOS.md +2 -2
- package/README.md +11 -17
- package/bootstrap.ts +118 -126
- package/docs/HOW-IT-WORKS.md +162 -0
- package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
- package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
- package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
- package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
- package/docs/adr/ADR-0005-hook-system-design.md +42 -0
- package/docs/adr/README.md +9 -0
- package/harness/codex/AUTOPILOT.md +35 -40
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +29 -29
- package/harness/lib/composer/fragments/02-delegation.md +5 -5
- package/harness/lib/composer/fragments/04-task-flow.md +13 -13
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +25 -25
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -72
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
- package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
- package/harness/lib/hooks/hooks.test.ts +160 -324
- package/harness/lib/hooks/index.ts +38 -42
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -119
- package/harness/lib/plans/plan-location.ts +134 -134
- package/harness/lib/routing/index.ts +21 -21
- package/harness/lib/routing/route-guidance.ts +147 -147
- package/harness/lib/routing/route-resolver.ts +58 -58
- package/harness/lib/routing/routing.test.ts +195 -195
- package/harness/lib/routing/skill-frontmatter.ts +125 -125
- package/harness/lib/routing/types.ts +52 -52
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +109 -109
- package/harness/skills/oh-fusion/SKILL.md +47 -47
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +5 -5
- package/package.json +56 -53
- package/harness/lib/background/background.test.ts +0 -216
- package/harness/lib/background/index.ts +0 -7
- package/harness/lib/background/interfaces.ts +0 -31
- package/harness/lib/background/manager.ts +0 -320
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
- package/harness/lib/hooks/builtins/subagent-failure-hook.ts +0 -93
- package/harness/lib/memory/index.ts +0 -18
- package/harness/lib/memory/interfaces.ts +0 -53
- package/harness/lib/memory/memory-manager.ts +0 -205
- package/harness/lib/memory/memory.test.ts +0 -485
- package/harness/lib/memory/plan-store.ts +0 -346
- package/harness/lib/recovery/handler.ts +0 -243
- package/harness/lib/recovery/index.ts +0 -14
- package/harness/lib/recovery/interfaces.ts +0 -48
- package/harness/lib/recovery/patterns.ts +0 -149
- package/harness/lib/recovery/recovery.test.ts +0 -312
- package/harness/lib/sanity/anomaly-tracker.ts +0 -127
- package/harness/lib/sanity/checker.ts +0 -189
- package/harness/lib/sanity/index.ts +0 -13
- package/harness/lib/sanity/interfaces.ts +0 -24
- package/harness/lib/sanity/sanity.test.ts +0 -472
- package/harness/lib/sync/file-watcher.ts +0 -175
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -533
- 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
|
+
}
|