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,72 +1,72 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// GuardConfig — centralized configuration for all loop/safety guards
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
export interface GuardConfig {
|
|
6
|
-
/** Max times the same skill can repeat in one chain before STOP */
|
|
7
|
-
maxSkillRepeats: number
|
|
8
|
-
/** Max consecutive unproductive hops before STOP (0 = disabled) */
|
|
9
|
-
maxUnproductiveHops: number
|
|
10
|
-
/** Max delegation (sub-agent) depth before STOP */
|
|
11
|
-
maxDelegationDepth: number
|
|
12
|
-
/** Consecutive anomalies before
|
|
13
|
-
maxConsecutiveAnomalies: number
|
|
14
|
-
/** Max subagent failures on same task before BLOCKER */
|
|
15
|
-
maxSubagentFailures: number
|
|
16
|
-
/** Enable progressive warning at thresholds before hard stop */
|
|
17
|
-
progressiveGuards: boolean
|
|
18
|
-
/** Ratio of limit at which to warn (e.g. 0.6 = 60%) */
|
|
19
|
-
progressiveWarnThreshold: number
|
|
20
|
-
/** Ratio of limit at which to escalate (e.g. 0.8 = 80%) */
|
|
21
|
-
progressiveEscalateThreshold: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
25
|
-
maxSkillRepeats: 5,
|
|
26
|
-
maxUnproductiveHops: 8,
|
|
27
|
-
maxDelegationDepth: 25,
|
|
28
|
-
maxConsecutiveAnomalies: 2,
|
|
29
|
-
maxSubagentFailures: 5,
|
|
30
|
-
progressiveGuards: true,
|
|
31
|
-
progressiveWarnThreshold: 0.6,
|
|
32
|
-
progressiveEscalateThreshold: 0.8,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type GuardLevel = "ok" | "warn" | "escalate" | "stop"
|
|
36
|
-
|
|
37
|
-
export interface GuardProgression {
|
|
38
|
-
level: GuardLevel
|
|
39
|
-
current: number
|
|
40
|
-
limit: number
|
|
41
|
-
/**
|
|
42
|
-
* If progressive guards are disabled: stop at limit, ok otherwise.
|
|
43
|
-
* If enabled: ok < warn% < escalate% < stop.
|
|
44
|
-
*/
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function checkGuardProgression(
|
|
48
|
-
current: number,
|
|
49
|
-
limit: number,
|
|
50
|
-
config: GuardConfig,
|
|
51
|
-
): GuardProgression {
|
|
52
|
-
if (!config.progressiveGuards || limit <= 0) {
|
|
53
|
-
return {
|
|
54
|
-
level: current >= limit ? "stop" as GuardLevel : "ok" as GuardLevel,
|
|
55
|
-
current,
|
|
56
|
-
limit,
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (current >= limit) return { level: "stop", current, limit }
|
|
60
|
-
if (current / limit >= config.progressiveEscalateThreshold) return { level: "escalate" as GuardLevel, current, limit }
|
|
61
|
-
if (current / limit >= config.progressiveWarnThreshold) return { level: "warn" as GuardLevel, current, limit }
|
|
62
|
-
return { level: "ok" as GuardLevel, current, limit }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Merge partial user config(s) with defaults.
|
|
67
|
-
* Priority: defaults → earlier args → later args (last wins).
|
|
68
|
-
* Supports single-arg calls and multi-override chains.
|
|
69
|
-
*/
|
|
70
|
-
export function mergeGuardConfig(...overrides: Array<Partial<GuardConfig> | undefined>): GuardConfig {
|
|
71
|
-
return Object.assign({}, DEFAULT_GUARD_CONFIG, ...overrides.filter(Boolean));
|
|
72
|
-
}
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// GuardConfig — centralized configuration for all loop/safety guards
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface GuardConfig {
|
|
6
|
+
/** Max times the same skill can repeat in one chain before STOP */
|
|
7
|
+
maxSkillRepeats: number
|
|
8
|
+
/** Max consecutive unproductive hops before STOP (0 = disabled) */
|
|
9
|
+
maxUnproductiveHops: number
|
|
10
|
+
/** Max delegation (sub-agent) depth before STOP */
|
|
11
|
+
maxDelegationDepth: number
|
|
12
|
+
/** Consecutive anomalies before guard escalation */
|
|
13
|
+
maxConsecutiveAnomalies: number
|
|
14
|
+
/** Max subagent failures on same task before BLOCKER */
|
|
15
|
+
maxSubagentFailures: number
|
|
16
|
+
/** Enable progressive warning at thresholds before hard stop */
|
|
17
|
+
progressiveGuards: boolean
|
|
18
|
+
/** Ratio of limit at which to warn (e.g. 0.6 = 60%) */
|
|
19
|
+
progressiveWarnThreshold: number
|
|
20
|
+
/** Ratio of limit at which to escalate (e.g. 0.8 = 80%) */
|
|
21
|
+
progressiveEscalateThreshold: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
25
|
+
maxSkillRepeats: 5,
|
|
26
|
+
maxUnproductiveHops: 8,
|
|
27
|
+
maxDelegationDepth: 25,
|
|
28
|
+
maxConsecutiveAnomalies: 2,
|
|
29
|
+
maxSubagentFailures: 5,
|
|
30
|
+
progressiveGuards: true,
|
|
31
|
+
progressiveWarnThreshold: 0.6,
|
|
32
|
+
progressiveEscalateThreshold: 0.8,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type GuardLevel = "ok" | "warn" | "escalate" | "stop"
|
|
36
|
+
|
|
37
|
+
export interface GuardProgression {
|
|
38
|
+
level: GuardLevel
|
|
39
|
+
current: number
|
|
40
|
+
limit: number
|
|
41
|
+
/**
|
|
42
|
+
* If progressive guards are disabled: stop at limit, ok otherwise.
|
|
43
|
+
* If enabled: ok < warn% < escalate% < stop.
|
|
44
|
+
*/
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkGuardProgression(
|
|
48
|
+
current: number,
|
|
49
|
+
limit: number,
|
|
50
|
+
config: GuardConfig,
|
|
51
|
+
): GuardProgression {
|
|
52
|
+
if (!config.progressiveGuards || limit <= 0) {
|
|
53
|
+
return {
|
|
54
|
+
level: current >= limit ? "stop" as GuardLevel : "ok" as GuardLevel,
|
|
55
|
+
current,
|
|
56
|
+
limit,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (current >= limit) return { level: "stop", current, limit }
|
|
60
|
+
if (current / limit >= config.progressiveEscalateThreshold) return { level: "escalate" as GuardLevel, current, limit }
|
|
61
|
+
if (current / limit >= config.progressiveWarnThreshold) return { level: "warn" as GuardLevel, current, limit }
|
|
62
|
+
return { level: "ok" as GuardLevel, current, limit }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Merge partial user config(s) with defaults.
|
|
67
|
+
* Priority: defaults → earlier args → later args (last wins).
|
|
68
|
+
* Supports single-arg calls and multi-override chains.
|
|
69
|
+
*/
|
|
70
|
+
export function mergeGuardConfig(...overrides: Array<Partial<GuardConfig> | undefined>): GuardConfig {
|
|
71
|
+
return Object.assign({}, DEFAULT_GUARD_CONFIG, ...overrides.filter(Boolean));
|
|
72
|
+
}
|
|
@@ -23,21 +23,21 @@ export const confidenceGateHook: RouteHook = {
|
|
|
23
23
|
errorHandling: "isolate",
|
|
24
24
|
},
|
|
25
25
|
|
|
26
|
-
async execute(context: HookContext, route: string) {
|
|
27
|
-
// Read confidence state from context if available
|
|
28
|
-
const confidenceLevel = context._confidenceLevel;
|
|
26
|
+
async execute(context: HookContext, route: string) {
|
|
27
|
+
// Read confidence state from context if available
|
|
28
|
+
const confidenceLevel = context._confidenceLevel;
|
|
29
29
|
|
|
30
30
|
if (!confidenceLevel) {
|
|
31
31
|
// No confidence gate info — pass through unchanged
|
|
32
32
|
return { result: HookResult.CONTINUE, modifiedRoute: route };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Store the confidence assessment for routing decisions
|
|
36
|
-
const state: ConfidenceGateState = {
|
|
37
|
-
level: confidenceLevel as ConfidenceGateState["level"],
|
|
38
|
-
exchanges: context._confidenceExchanges ?? 0,
|
|
39
|
-
lastAction: "assessed",
|
|
40
|
-
};
|
|
35
|
+
// Store the confidence assessment for routing decisions
|
|
36
|
+
const state: ConfidenceGateState = {
|
|
37
|
+
level: confidenceLevel as ConfidenceGateState["level"],
|
|
38
|
+
exchanges: context._confidenceExchanges ?? 0,
|
|
39
|
+
lastAction: "assessed",
|
|
40
|
+
};
|
|
41
41
|
|
|
42
42
|
// HIGH confidence: proceed without modification
|
|
43
43
|
if (state.level === "HIGH") {
|
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { HookPhase, HookResult } from "../types.ts";
|
|
3
|
-
import type { HookContext, PostToolUseHook } from "../types.ts";
|
|
4
|
-
import { readSkillFrontmatter, resolveRoute } from "../../routing/index.ts";
|
|
5
|
-
import type { RouteEvidence } from "../../routing/index.ts";
|
|
6
|
-
import { ROUTE_GUIDANCE_PREFIX } from "../../routing/index.ts";
|
|
7
|
-
import {
|
|
8
|
-
ROUTE_ACTIONS,
|
|
9
|
-
ROUTE_OUTCOMES,
|
|
10
|
-
ROUTE_VERIFICATIONS,
|
|
11
|
-
ROUTE_WORK_TYPES,
|
|
12
|
-
} from "../../routing/types.ts";
|
|
13
|
-
|
|
14
|
-
const ROUTE_EVIDENCE_PREFIX = "ROUTE_EVIDENCE:";
|
|
15
|
-
|
|
16
|
-
function isRouteOutcome(value: unknown): value is RouteEvidence["outcome"] {
|
|
17
|
-
return typeof value === "string" && ROUTE_OUTCOMES.includes(value as RouteEvidence["outcome"]);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function isRouteVerification(value: unknown): value is NonNullable<RouteEvidence["verification"]> {
|
|
21
|
-
return typeof value === "string" && ROUTE_VERIFICATIONS.includes(value as NonNullable<RouteEvidence["verification"]>);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function isRouteAction(value: unknown): value is NonNullable<RouteEvidence["action"]> {
|
|
25
|
-
return typeof value === "string" && ROUTE_ACTIONS.includes(value as NonNullable<RouteEvidence["action"]>);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isRouteWork(value: unknown): value is NonNullable<RouteEvidence["work"]> {
|
|
29
|
-
return typeof value === "string" && ROUTE_WORK_TYPES.includes(value as NonNullable<RouteEvidence["work"]>);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseRouteEvidence(output: string): RouteEvidence | null {
|
|
33
|
-
const evidenceLine = output
|
|
34
|
-
.split(/\r?\n/)
|
|
35
|
-
.map((line) => line.trim())
|
|
36
|
-
.find((line) => line.startsWith(ROUTE_EVIDENCE_PREFIX));
|
|
37
|
-
|
|
38
|
-
if (!evidenceLine) return null;
|
|
39
|
-
|
|
40
|
-
const raw = evidenceLine.slice(ROUTE_EVIDENCE_PREFIX.length).trim();
|
|
41
|
-
if (!raw) return null;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const parsed = JSON.parse(raw) as Partial<RouteEvidence>;
|
|
45
|
-
if (!isRouteOutcome(parsed.outcome)) return null;
|
|
46
|
-
if (parsed.verification !== undefined && !isRouteVerification(parsed.verification)) return null;
|
|
47
|
-
if (parsed.action !== undefined && !isRouteAction(parsed.action)) return null;
|
|
48
|
-
if (parsed.work !== undefined && !isRouteWork(parsed.work)) return null;
|
|
49
|
-
if (parsed.target !== undefined && typeof parsed.target !== "string") return null;
|
|
50
|
-
if (parsed.reason !== undefined && typeof parsed.reason !== "string") return null;
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
outcome: parsed.outcome,
|
|
54
|
-
...(parsed.verification ? { verification: parsed.verification } : {}),
|
|
55
|
-
...(parsed.action ? { action: parsed.action } : {}),
|
|
56
|
-
...(parsed.work ? { work: parsed.work } : {}),
|
|
57
|
-
...(parsed.target ? { target: parsed.target } : {}),
|
|
58
|
-
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
59
|
-
};
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const dynamicRouteHook: PostToolUseHook = {
|
|
66
|
-
metadata: {
|
|
67
|
-
name: "dynamic-route",
|
|
68
|
-
priority: 20,
|
|
69
|
-
phase: HookPhase.LATE,
|
|
70
|
-
dependencies: [],
|
|
71
|
-
errorHandling: "isolate",
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
async execute(context: HookContext, output: string) {
|
|
75
|
-
const evidence = parseRouteEvidence(output);
|
|
76
|
-
const skillsDir = typeof context._routingSkillsDir === "string" ? context._routingSkillsDir : undefined;
|
|
77
|
-
|
|
78
|
-
if (!evidence || !skillsDir || !context.agent) {
|
|
79
|
-
return { result: HookResult.CONTINUE };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const skillFilePath = path.join(skillsDir, context.agent, "SKILL.md");
|
|
83
|
-
const frontmatter = readSkillFrontmatter(skillFilePath);
|
|
84
|
-
if (!frontmatter) {
|
|
85
|
-
return { result: HookResult.CONTINUE };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const resolution = resolveRoute(frontmatter.route, evidence);
|
|
89
|
-
const guidance = `${ROUTE_GUIDANCE_PREFIX} ${JSON.stringify(resolution)}`;
|
|
90
|
-
const modifiedOutput = output.includes(ROUTE_GUIDANCE_PREFIX)
|
|
91
|
-
? output
|
|
92
|
-
: `${output.trimEnd()}\n${guidance}`.trim();
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
result: HookResult.INJECT,
|
|
96
|
-
modifiedOutput,
|
|
97
|
-
};
|
|
98
|
-
},
|
|
99
|
-
};
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { HookPhase, HookResult } from "../types.ts";
|
|
3
|
+
import type { HookContext, PostToolUseHook } from "../types.ts";
|
|
4
|
+
import { readSkillFrontmatter, resolveRoute } from "../../routing/index.ts";
|
|
5
|
+
import type { RouteEvidence } from "../../routing/index.ts";
|
|
6
|
+
import { ROUTE_GUIDANCE_PREFIX } from "../../routing/index.ts";
|
|
7
|
+
import {
|
|
8
|
+
ROUTE_ACTIONS,
|
|
9
|
+
ROUTE_OUTCOMES,
|
|
10
|
+
ROUTE_VERIFICATIONS,
|
|
11
|
+
ROUTE_WORK_TYPES,
|
|
12
|
+
} from "../../routing/types.ts";
|
|
13
|
+
|
|
14
|
+
const ROUTE_EVIDENCE_PREFIX = "ROUTE_EVIDENCE:";
|
|
15
|
+
|
|
16
|
+
function isRouteOutcome(value: unknown): value is RouteEvidence["outcome"] {
|
|
17
|
+
return typeof value === "string" && ROUTE_OUTCOMES.includes(value as RouteEvidence["outcome"]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isRouteVerification(value: unknown): value is NonNullable<RouteEvidence["verification"]> {
|
|
21
|
+
return typeof value === "string" && ROUTE_VERIFICATIONS.includes(value as NonNullable<RouteEvidence["verification"]>);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRouteAction(value: unknown): value is NonNullable<RouteEvidence["action"]> {
|
|
25
|
+
return typeof value === "string" && ROUTE_ACTIONS.includes(value as NonNullable<RouteEvidence["action"]>);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRouteWork(value: unknown): value is NonNullable<RouteEvidence["work"]> {
|
|
29
|
+
return typeof value === "string" && ROUTE_WORK_TYPES.includes(value as NonNullable<RouteEvidence["work"]>);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseRouteEvidence(output: string): RouteEvidence | null {
|
|
33
|
+
const evidenceLine = output
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.find((line) => line.startsWith(ROUTE_EVIDENCE_PREFIX));
|
|
37
|
+
|
|
38
|
+
if (!evidenceLine) return null;
|
|
39
|
+
|
|
40
|
+
const raw = evidenceLine.slice(ROUTE_EVIDENCE_PREFIX.length).trim();
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw) as Partial<RouteEvidence>;
|
|
45
|
+
if (!isRouteOutcome(parsed.outcome)) return null;
|
|
46
|
+
if (parsed.verification !== undefined && !isRouteVerification(parsed.verification)) return null;
|
|
47
|
+
if (parsed.action !== undefined && !isRouteAction(parsed.action)) return null;
|
|
48
|
+
if (parsed.work !== undefined && !isRouteWork(parsed.work)) return null;
|
|
49
|
+
if (parsed.target !== undefined && typeof parsed.target !== "string") return null;
|
|
50
|
+
if (parsed.reason !== undefined && typeof parsed.reason !== "string") return null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
outcome: parsed.outcome,
|
|
54
|
+
...(parsed.verification ? { verification: parsed.verification } : {}),
|
|
55
|
+
...(parsed.action ? { action: parsed.action } : {}),
|
|
56
|
+
...(parsed.work ? { work: parsed.work } : {}),
|
|
57
|
+
...(parsed.target ? { target: parsed.target } : {}),
|
|
58
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const dynamicRouteHook: PostToolUseHook = {
|
|
66
|
+
metadata: {
|
|
67
|
+
name: "dynamic-route",
|
|
68
|
+
priority: 20,
|
|
69
|
+
phase: HookPhase.LATE,
|
|
70
|
+
dependencies: [],
|
|
71
|
+
errorHandling: "isolate",
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async execute(context: HookContext, output: string) {
|
|
75
|
+
const evidence = parseRouteEvidence(output);
|
|
76
|
+
const skillsDir = typeof context._routingSkillsDir === "string" ? context._routingSkillsDir : undefined;
|
|
77
|
+
|
|
78
|
+
if (!evidence || !skillsDir || !context.agent) {
|
|
79
|
+
return { result: HookResult.CONTINUE };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const skillFilePath = path.join(skillsDir, context.agent, "SKILL.md");
|
|
83
|
+
const frontmatter = readSkillFrontmatter(skillFilePath);
|
|
84
|
+
if (!frontmatter) {
|
|
85
|
+
return { result: HookResult.CONTINUE };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resolution = resolveRoute(frontmatter.route, evidence);
|
|
89
|
+
const guidance = `${ROUTE_GUIDANCE_PREFIX} ${JSON.stringify(resolution)}`;
|
|
90
|
+
const modifiedOutput = output.includes(ROUTE_GUIDANCE_PREFIX)
|
|
91
|
+
? output
|
|
92
|
+
: `${output.trimEnd()}\n${guidance}`.trim();
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
result: HookResult.INJECT,
|
|
96
|
+
modifiedOutput,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { HookPhase, HookResult } from "../types.ts";
|
|
2
|
-
import type { HookContext, RouteHook } from "../types.ts";
|
|
3
|
-
|
|
4
|
-
export const nextRouteHook: RouteHook = {
|
|
5
|
-
metadata: {
|
|
6
|
-
name: "next-route",
|
|
7
|
-
priority: 90,
|
|
8
|
-
phase: HookPhase.EARLY,
|
|
9
|
-
dependencies: [],
|
|
10
|
-
errorHandling: "isolate",
|
|
11
|
-
},
|
|
12
|
-
|
|
13
|
-
async execute(context: HookContext, route: string) {
|
|
14
|
-
const nextRoute = context._nextRoute?.selected;
|
|
15
|
-
if (!nextRoute || nextRoute === route) {
|
|
16
|
-
return { result: HookResult.CONTINUE, modifiedRoute: route };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
result: HookResult.CONTINUE,
|
|
21
|
-
modifiedRoute: nextRoute,
|
|
22
|
-
};
|
|
23
|
-
},
|
|
24
|
-
};
|
|
1
|
+
import { HookPhase, HookResult } from "../types.ts";
|
|
2
|
+
import type { HookContext, RouteHook } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export const nextRouteHook: RouteHook = {
|
|
5
|
+
metadata: {
|
|
6
|
+
name: "next-route",
|
|
7
|
+
priority: 90,
|
|
8
|
+
phase: HookPhase.EARLY,
|
|
9
|
+
dependencies: [],
|
|
10
|
+
errorHandling: "isolate",
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async execute(context: HookContext, route: string) {
|
|
14
|
+
const nextRoute = context._nextRoute?.selected;
|
|
15
|
+
if (!nextRoute || nextRoute === route) {
|
|
16
|
+
return { result: HookResult.CONTINUE, modifiedRoute: route };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
result: HookResult.CONTINUE,
|
|
21
|
+
modifiedRoute: nextRoute,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// If missing, inject "create plan first" instruction.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
import { HookPhase, HookResult } from "../types.ts";
|
|
9
|
-
import type { HookContext, PreToolUseHook } from "../types.ts";
|
|
10
|
-
import { resolvePlanAccess } from "../../plans/plan-location.ts";
|
|
8
|
+
import { HookPhase, HookResult } from "../types.ts";
|
|
9
|
+
import type { HookContext, PreToolUseHook } from "../types.ts";
|
|
10
|
+
import { resolvePlanAccess } from "../../plans/plan-location.ts";
|
|
11
11
|
|
|
12
12
|
export const planCheckHook: PreToolUseHook = {
|
|
13
13
|
metadata: {
|
|
@@ -18,8 +18,8 @@ export const planCheckHook: PreToolUseHook = {
|
|
|
18
18
|
errorHandling: "propagate",
|
|
19
19
|
},
|
|
20
20
|
|
|
21
|
-
async execute(context: HookContext) {
|
|
22
|
-
const planFile = resolvePlanAccess(context.directory)?.path ?? null;
|
|
21
|
+
async execute(context: HookContext) {
|
|
22
|
+
const planFile = resolvePlanAccess(context.directory)?.path ?? null;
|
|
23
23
|
|
|
24
24
|
if (!planFile) {
|
|
25
25
|
return {
|