@dungle-scrubs/harness-cli-normalizer 0.1.1

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 (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/execution/channel.d.ts +33 -0
  4. package/dist/execution/channel.d.ts.map +1 -0
  5. package/dist/execution/channel.js +86 -0
  6. package/dist/execution/channel.js.map +1 -0
  7. package/dist/execution/decode.d.ts +16 -0
  8. package/dist/execution/decode.d.ts.map +1 -0
  9. package/dist/execution/decode.js +60 -0
  10. package/dist/execution/decode.js.map +1 -0
  11. package/dist/execution/deps.d.ts +53 -0
  12. package/dist/execution/deps.d.ts.map +1 -0
  13. package/dist/execution/deps.js +8 -0
  14. package/dist/execution/deps.js.map +1 -0
  15. package/dist/execution/events.d.ts +54 -0
  16. package/dist/execution/events.d.ts.map +1 -0
  17. package/dist/execution/events.js +2 -0
  18. package/dist/execution/events.js.map +1 -0
  19. package/dist/execution/index.d.ts +19 -0
  20. package/dist/execution/index.d.ts.map +1 -0
  21. package/dist/execution/index.js +19 -0
  22. package/dist/execution/index.js.map +1 -0
  23. package/dist/execution/lines.d.ts +21 -0
  24. package/dist/execution/lines.d.ts.map +1 -0
  25. package/dist/execution/lines.js +70 -0
  26. package/dist/execution/lines.js.map +1 -0
  27. package/dist/execution/node-deps.d.ts +13 -0
  28. package/dist/execution/node-deps.d.ts.map +1 -0
  29. package/dist/execution/node-deps.js +164 -0
  30. package/dist/execution/node-deps.js.map +1 -0
  31. package/dist/execution/open-session.d.ts +31 -0
  32. package/dist/execution/open-session.d.ts.map +1 -0
  33. package/dist/execution/open-session.js +360 -0
  34. package/dist/execution/open-session.js.map +1 -0
  35. package/dist/execution/stream-turn.d.ts +43 -0
  36. package/dist/execution/stream-turn.d.ts.map +1 -0
  37. package/dist/execution/stream-turn.js +283 -0
  38. package/dist/execution/stream-turn.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/interpretation/argv.d.ts +35 -0
  44. package/dist/interpretation/argv.d.ts.map +1 -0
  45. package/dist/interpretation/argv.js +147 -0
  46. package/dist/interpretation/argv.js.map +1 -0
  47. package/dist/interpretation/capabilities.d.ts +19 -0
  48. package/dist/interpretation/capabilities.d.ts.map +1 -0
  49. package/dist/interpretation/capabilities.js +25 -0
  50. package/dist/interpretation/capabilities.js.map +1 -0
  51. package/dist/interpretation/content.d.ts +33 -0
  52. package/dist/interpretation/content.d.ts.map +1 -0
  53. package/dist/interpretation/content.js +169 -0
  54. package/dist/interpretation/content.js.map +1 -0
  55. package/dist/interpretation/context.d.ts +17 -0
  56. package/dist/interpretation/context.d.ts.map +1 -0
  57. package/dist/interpretation/context.js +16 -0
  58. package/dist/interpretation/context.js.map +1 -0
  59. package/dist/interpretation/dimensions.d.ts +13 -0
  60. package/dist/interpretation/dimensions.d.ts.map +1 -0
  61. package/dist/interpretation/dimensions.js +6 -0
  62. package/dist/interpretation/dimensions.js.map +1 -0
  63. package/dist/interpretation/identity.d.ts +32 -0
  64. package/dist/interpretation/identity.d.ts.map +1 -0
  65. package/dist/interpretation/identity.js +44 -0
  66. package/dist/interpretation/identity.js.map +1 -0
  67. package/dist/interpretation/index.d.ts +23 -0
  68. package/dist/interpretation/index.d.ts.map +1 -0
  69. package/dist/interpretation/index.js +23 -0
  70. package/dist/interpretation/index.js.map +1 -0
  71. package/dist/interpretation/limits.d.ts +20 -0
  72. package/dist/interpretation/limits.d.ts.map +1 -0
  73. package/dist/interpretation/limits.js +35 -0
  74. package/dist/interpretation/limits.js.map +1 -0
  75. package/dist/interpretation/parse-resume.d.ts +28 -0
  76. package/dist/interpretation/parse-resume.d.ts.map +1 -0
  77. package/dist/interpretation/parse-resume.js +91 -0
  78. package/dist/interpretation/parse-resume.js.map +1 -0
  79. package/dist/interpretation/presence.d.ts +16 -0
  80. package/dist/interpretation/presence.d.ts.map +1 -0
  81. package/dist/interpretation/presence.js +32 -0
  82. package/dist/interpretation/presence.js.map +1 -0
  83. package/dist/interpretation/resume-last.d.ts +32 -0
  84. package/dist/interpretation/resume-last.d.ts.map +1 -0
  85. package/dist/interpretation/resume-last.js +70 -0
  86. package/dist/interpretation/resume-last.js.map +1 -0
  87. package/dist/interpretation/session-id.d.ts +22 -0
  88. package/dist/interpretation/session-id.d.ts.map +1 -0
  89. package/dist/interpretation/session-id.js +29 -0
  90. package/dist/interpretation/session-id.js.map +1 -0
  91. package/dist/interpretation/session-input.d.ts +13 -0
  92. package/dist/interpretation/session-input.d.ts.map +1 -0
  93. package/dist/interpretation/session-input.js +32 -0
  94. package/dist/interpretation/session-input.js.map +1 -0
  95. package/dist/interpretation/shape.d.ts +17 -0
  96. package/dist/interpretation/shape.d.ts.map +1 -0
  97. package/dist/interpretation/shape.js +54 -0
  98. package/dist/interpretation/shape.js.map +1 -0
  99. package/dist/interpretation/store.d.ts +16 -0
  100. package/dist/interpretation/store.d.ts.map +1 -0
  101. package/dist/interpretation/store.js +27 -0
  102. package/dist/interpretation/store.js.map +1 -0
  103. package/dist/interpretation/versions.d.ts +16 -0
  104. package/dist/interpretation/versions.d.ts.map +1 -0
  105. package/dist/interpretation/versions.js +44 -0
  106. package/dist/interpretation/versions.js.map +1 -0
  107. package/dist/interpretation/vocabulary.d.ts +29 -0
  108. package/dist/interpretation/vocabulary.d.ts.map +1 -0
  109. package/dist/interpretation/vocabulary.js +55 -0
  110. package/dist/interpretation/vocabulary.js.map +1 -0
  111. package/dist/knowledge/claude-code.d.ts +8 -0
  112. package/dist/knowledge/claude-code.d.ts.map +1 -0
  113. package/dist/knowledge/claude-code.js +132 -0
  114. package/dist/knowledge/claude-code.js.map +1 -0
  115. package/dist/knowledge/codex.d.ts +9 -0
  116. package/dist/knowledge/codex.d.ts.map +1 -0
  117. package/dist/knowledge/codex.js +106 -0
  118. package/dist/knowledge/codex.js.map +1 -0
  119. package/dist/knowledge/descriptor.d.ts +211 -0
  120. package/dist/knowledge/descriptor.d.ts.map +1 -0
  121. package/dist/knowledge/descriptor.js +20 -0
  122. package/dist/knowledge/descriptor.js.map +1 -0
  123. package/dist/knowledge/index.d.ts +19 -0
  124. package/dist/knowledge/index.d.ts.map +1 -0
  125. package/dist/knowledge/index.js +19 -0
  126. package/dist/knowledge/index.js.map +1 -0
  127. package/dist/knowledge/matchers.d.ts +10 -0
  128. package/dist/knowledge/matchers.d.ts.map +1 -0
  129. package/dist/knowledge/matchers.js +11 -0
  130. package/dist/knowledge/matchers.js.map +1 -0
  131. package/dist/knowledge/muse.d.ts +12 -0
  132. package/dist/knowledge/muse.d.ts.map +1 -0
  133. package/dist/knowledge/muse.js +94 -0
  134. package/dist/knowledge/muse.js.map +1 -0
  135. package/dist/knowledge/overrides.d.ts +11 -0
  136. package/dist/knowledge/overrides.d.ts.map +1 -0
  137. package/dist/knowledge/overrides.js +166 -0
  138. package/dist/knowledge/overrides.js.map +1 -0
  139. package/dist/knowledge/pi.d.ts +11 -0
  140. package/dist/knowledge/pi.d.ts.map +1 -0
  141. package/dist/knowledge/pi.js +97 -0
  142. package/dist/knowledge/pi.js.map +1 -0
  143. package/package.json +69 -0
  144. package/src/execution/channel.ts +86 -0
  145. package/src/execution/decode.ts +79 -0
  146. package/src/execution/deps.ts +59 -0
  147. package/src/execution/events.ts +37 -0
  148. package/src/execution/index.ts +18 -0
  149. package/src/execution/lines.ts +71 -0
  150. package/src/execution/node-deps.ts +179 -0
  151. package/src/execution/open-session.ts +392 -0
  152. package/src/execution/stream-turn.ts +310 -0
  153. package/src/index.ts +7 -0
  154. package/src/interpretation/argv.ts +188 -0
  155. package/src/interpretation/capabilities.ts +51 -0
  156. package/src/interpretation/content.ts +191 -0
  157. package/src/interpretation/context.ts +28 -0
  158. package/src/interpretation/dimensions.ts +20 -0
  159. package/src/interpretation/identity.ts +81 -0
  160. package/src/interpretation/index.ts +22 -0
  161. package/src/interpretation/limits.ts +63 -0
  162. package/src/interpretation/parse-resume.ts +117 -0
  163. package/src/interpretation/presence.ts +49 -0
  164. package/src/interpretation/resume-last.ts +93 -0
  165. package/src/interpretation/session-id.ts +35 -0
  166. package/src/interpretation/session-input.ts +45 -0
  167. package/src/interpretation/shape.ts +55 -0
  168. package/src/interpretation/store.ts +43 -0
  169. package/src/interpretation/versions.ts +43 -0
  170. package/src/interpretation/vocabulary.ts +74 -0
  171. package/src/knowledge/claude-code.ts +132 -0
  172. package/src/knowledge/codex.ts +106 -0
  173. package/src/knowledge/descriptor.ts +217 -0
  174. package/src/knowledge/index.ts +23 -0
  175. package/src/knowledge/matchers.ts +19 -0
  176. package/src/knowledge/muse.ts +94 -0
  177. package/src/knowledge/overrides.ts +182 -0
  178. package/src/knowledge/pi.ts +97 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The claude-code descriptor: facts about the `claude` CLI as data, verified
3
+ * against claude 2.1.227 and the 00-chat-substrate spike evidence (A-001,
4
+ * A-002, A-005). No process logic lives here.
5
+ */
6
+ import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
7
+ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
8
+
9
+ export const claudeCode: HarnessDescriptor = deepFreeze({
10
+ name: "claude",
11
+ bin: "claude",
12
+ verifiedAgainst: "2.1.227",
13
+ versionSource: { kind: "npm", package: "@anthropic-ai/claude-code" },
14
+ launch: {
15
+ baseFlags: ["-p"],
16
+ subcommands: [],
17
+ promptStyle: "positional",
18
+ toolsFlag: "--allowedTools",
19
+ // A headless turn launches with the full stream-json output set so the
20
+ // runner can decode identity/limits and stream token deltas; bare -p
21
+ // (granularity none) is a degraded invocation this builder never emits.
22
+ streamFlags: ["--output-format", "stream-json", "--verbose", "--include-partial-messages"],
23
+ idFlag: "--session-id",
24
+ },
25
+ resume: {
26
+ // A-005: claude resumes are id-stable - the caller-assigned id survives
27
+ // every resume, so there is no rotation handling and forking is only the
28
+ // explicit --fork-session flag (deliberate branching, never a default).
29
+ style: "flag",
30
+ flag: "--resume",
31
+ aliases: ["-r"],
32
+ idShape: UUID_SHAPE,
33
+ onMissing: "error",
34
+ extraFlags: ["-p"],
35
+ },
36
+ sessionMode: {
37
+ // A-001: one process, many turns; `result` delimits turns; mid-turn sends
38
+ // queue. --setting-sources project isolates the child from user-level
39
+ // hooks (D-025). Token deltas require this exact output flag set.
40
+ flags: [
41
+ "--input-format",
42
+ "stream-json",
43
+ "--output-format",
44
+ "stream-json",
45
+ "--include-partial-messages",
46
+ "--verbose",
47
+ "--setting-sources",
48
+ "project",
49
+ ],
50
+ idFlag: "--session-id",
51
+ input: { kind: "claude-sdk-user-message" },
52
+ },
53
+ output: {
54
+ // --output-format/--include-partial-messages only work with --print, so
55
+ // -p is part of the pin, not an accident of the builders.
56
+ pins: [
57
+ {
58
+ flags: ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages"],
59
+ granularity: "token",
60
+ },
61
+ ],
62
+ floor: "none",
63
+ flagAliases: { "--print": "-p" },
64
+ },
65
+ identity: {
66
+ authority: "caller-assigned",
67
+ announce: { match: { type: "system", subtype: "init" }, idField: "session_id" },
68
+ },
69
+ limitMatchers: [
70
+ // Observed phrasings from the live CLI (ported from lucid v1's limits.ts):
71
+ // "You've hit your session limit · resets 6:30pm"
72
+ [/you'?ve hit your session limit/i, "session-limit"],
73
+ // "You've hit your weekly limit · resets 2am (Asia/Bangkok)"
74
+ [/you'?ve hit your weekly limit/i, "weekly-limit"],
75
+ ...SHARED_LIMIT_MATCHERS,
76
+ ],
77
+ authMatchers: [
78
+ // A detached process cannot read Keychain creds, and misreading that as
79
+ // "not logged in" sends the human to redo a login that was never broken.
80
+ [/oauth session expired|could not be refreshed/i, "expired"],
81
+ [/failed to authenticate/i, "expired"],
82
+ [/not logged in|please run \/login/i, "not-logged-in"],
83
+ ...SHARED_AUTH_MATCHERS,
84
+ ],
85
+ autonomy: { flag: "--dangerously-skip-permissions" },
86
+ vocabulary: {
87
+ modelFlag: "--model",
88
+ models: ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001"],
89
+ aliases: {
90
+ fable: "claude-fable-5",
91
+ opus: "claude-opus-5",
92
+ sonnet: "claude-sonnet-5",
93
+ haiku: "claude-haiku-4-5-20251001",
94
+ },
95
+ efforts: ["low", "medium", "high", "xhigh", "max"],
96
+ // Effort is an in-session command for claude, not a launch flag.
97
+ effortFlag: null,
98
+ extensible: false,
99
+ },
100
+ store: {
101
+ // Verified against the A-001 fixture's memory_paths slug and real
102
+ // ~/.claude/projects entries: '/', '.' -> '-'; '_' preserved.
103
+ template: "{home}/.claude/projects/{cwdSlug}/{sessionId}.jsonl",
104
+ cwdSlug: "dash-separators",
105
+ },
106
+ contextHook: {
107
+ // claude statusline payload: { context_window: { used_percentage } }.
108
+ // This arrives on the statusline channel, never on stream-json stdout -
109
+ // route accordingly, do not call per stdout line.
110
+ object: "context_window",
111
+ usedPctField: "used_percentage",
112
+ },
113
+ resumeLast: null,
114
+ provider: null,
115
+ stdin: "inherit",
116
+ // D-025: project-only setting sources is claude's discovery-isolation
117
+ // spelling; the child loads no user-level hooks or skills.
118
+ discoveryDisableFlags: ["--setting-sources", "project"],
119
+ presence: {
120
+ headlessMarkers: ["-p", "--print"],
121
+ },
122
+ capabilities: {
123
+ vision: true,
124
+ images: true,
125
+ streamingByMode: {
126
+ "headless-turn": "token",
127
+ "headless-session": "token",
128
+ interactive: "message",
129
+ },
130
+ session: true,
131
+ },
132
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The codex descriptor: facts about the `codex` CLI as data, verified
3
+ * against codex-cli 0.147.0 and lucid v1's registry. Descriptor groundwork
4
+ * only (D-003): not exercised through the chat protocol until the claude
5
+ * vertical slice is green.
6
+ */
7
+ import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
8
+ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
9
+
10
+ export const codexCli: HarnessDescriptor = deepFreeze({
11
+ name: "codex",
12
+ bin: "codex",
13
+ verifiedAgainst: "0.147.0",
14
+ versionSource: { kind: "npm", package: "@openai/codex" },
15
+ launch: {
16
+ // exec --json emits structured item events; without --json, identity
17
+ // discovery is blind (v1: requiredArgument "--json"). The sandbox grant
18
+ // is v1's proven spawn shape - codex's built-in default is read-only,
19
+ // under which a non-autonomy turn cannot write files.
20
+ // --skip-git-repo-check: codex exec refuses to run outside a trusted
21
+ // git dir without it (verified 0.147.0). cwd targeting is the spawner's
22
+ // job (spawn opts.cwd), not descriptor data.
23
+ baseFlags: ["exec", "--json", "--sandbox", "workspace-write", "--skip-git-repo-check"],
24
+ subcommands: ["exec"],
25
+ promptStyle: "positional",
26
+ toolsFlag: null,
27
+ streamFlags: [],
28
+ // Codex mints its own thread id; there is nothing to assign at launch.
29
+ idFlag: null,
30
+ },
31
+ resume: {
32
+ // `codex exec resume <id> [--json] <prompt>` - the resume word is a
33
+ // subcommand of exec (verified: `codex exec resume --help`).
34
+ style: "positional",
35
+ flag: "resume",
36
+ aliases: [],
37
+ idShape: UUID_SHAPE,
38
+ onMissing: "error",
39
+ // `codex exec resume` accepts --json and --skip-git-repo-check but
40
+ // REJECTS --sandbox (verified 0.147.0: "unexpected argument").
41
+ extraFlags: ["--json", "--skip-git-repo-check"],
42
+ },
43
+ sessionMode: null,
44
+ output: {
45
+ // exec --json emits item-level events (message granularity); a bare
46
+ // exec emits nothing structured at all.
47
+ pins: [{ flags: ["--json"], granularity: "message" }],
48
+ floor: "none",
49
+ flagAliases: {},
50
+ },
51
+ identity: {
52
+ // Codex mints its own thread id and announces it on stdout-jsonl.
53
+ authority: "harness-minted",
54
+ announce: { match: { type: "thread.started" }, idField: "thread_id" },
55
+ },
56
+ limitMatchers: [...SHARED_LIMIT_MATCHERS],
57
+ authMatchers: [[/run codex login/i, "not-logged-in"], ...SHARED_AUTH_MATCHERS],
58
+ // Accepted by codex 0.147.0 as a hidden alias (not in --help).
59
+ autonomy: { flag: "--yolo" },
60
+ vocabulary: {
61
+ modelFlag: "--model",
62
+ models: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
63
+ aliases: {},
64
+ efforts: ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"],
65
+ // Codex constrains ladders per model generation (v1 registry).
66
+ effortsByModel: {
67
+ "gpt-5.5": ["minimal", "low", "medium", "high"],
68
+ "gpt-5.6-sol": ["medium", "high", "xhigh", "max", "ultra"],
69
+ "gpt-5.6-terra": ["medium", "high", "xhigh", "max", "ultra"],
70
+ "gpt-5.6-luna": ["medium", "high", "xhigh", "max", "ultra"],
71
+ },
72
+ // Reasoning effort is a config key (-c model_reasoning_effort=...), not
73
+ // a plain flag; argv-level insertion has no spelling to use.
74
+ effortFlag: null,
75
+ extensible: false,
76
+ },
77
+ store: {
78
+ // ~/.codex/sessions/YYYY/MM/DD/rollout-<stamp>-<threadId>.jsonl - the
79
+ // date/stamp components need a store scan, so the template names the
80
+ // sessions root; the execution layer resolves the rollout file.
81
+ template: "{home}/.codex/sessions",
82
+ cwdSlug: "verbatim",
83
+ },
84
+ contextHook: null,
85
+ // Valid only in the `exec resume` context: `codex exec resume --last`.
86
+ resumeLast: { flag: "--last" },
87
+ provider: null,
88
+ // codex exec appends piped stdin as a <stdin> block and can block on an
89
+ // open stdin - close it (verified 0.147.0: "Reading additional input
90
+ // from stdin...").
91
+ stdin: "close-required",
92
+ discoveryDisableFlags: [],
93
+ presence: {
94
+ headlessMarkers: ["exec"],
95
+ },
96
+ capabilities: {
97
+ vision: true,
98
+ images: true,
99
+ streamingByMode: {
100
+ "headless-turn": "message",
101
+ "headless-session": "none",
102
+ interactive: "none",
103
+ },
104
+ session: false,
105
+ },
106
+ });
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Descriptor types: the shape of what is KNOWN about a harness CLI, as pure
3
+ * data. Interpretation functions consume these; nothing here executes.
4
+ */
5
+
6
+ export const HARNESS_NAMES = ["claude", "codex", "pi", "muse"] as const;
7
+ export type HarnessName = (typeof HARNESS_NAMES)[number];
8
+
9
+ /** Descriptors are process-wide defaults shared by reference into merged
10
+ * override sets - freezing makes an accidental in-place edit throw instead
11
+ * of corrupting every consumer. */
12
+ /** A 36-char UUID - how every supported harness names its sessions. */
13
+ export const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+
15
+ export const deepFreeze = <T>(value: T): T => {
16
+ if (typeof value === "object" && value !== null) {
17
+ for (const inner of Object.values(value)) deepFreeze(inner);
18
+ Object.freeze(value);
19
+ }
20
+ return value;
21
+ };
22
+
23
+ export type StreamingGranularity = "token" | "message" | "none";
24
+
25
+ export type HarnessMode = "headless-turn" | "headless-session" | "interactive";
26
+
27
+ export const SESSION_INPUT_KINDS = ["claude-sdk-user-message"] as const;
28
+ export type SessionInputKind = (typeof SESSION_INPUT_KINDS)[number];
29
+
30
+ export interface SessionInputContract {
31
+ readonly kind: SessionInputKind;
32
+ }
33
+
34
+ /** Consumers branch on these (session-limit: wait for reset; weekly-limit:
35
+ * route elsewhere), so the vocabulary is closed - a descriptor cannot invent
36
+ * a code a consumer has no arm for. */
37
+ export type LimitCode = "usage-limit" | "session-limit" | "weekly-limit" | "credits" | "quota";
38
+
39
+ /** Auth walls are separate from usage limits because the remedy is entirely
40
+ * different (re-auth vs wait), and a detached process misreading one as the
41
+ * other sends the human to fix the wrong thing. */
42
+ export type AuthFailureKind = "not-logged-in" | "expired" | "invalid-key";
43
+
44
+ export interface HarnessDescriptor {
45
+ readonly name: HarnessName;
46
+ readonly bin: string;
47
+ /** The CLI version every fact in this descriptor - argv shapes, event
48
+ * vocabularies, capability claims, resume semantics - was verified
49
+ * against, as `<bin> --version` reports it. This is the anchor for the
50
+ * harness-update pipeline: CI compares the published/installed version to
51
+ * this, and a mismatch means the descriptor's facts are unverified for the
52
+ * new version (drift possible, or a capability the descriptor says is
53
+ * absent may now exist). Bump it only when the facts have been re-verified
54
+ * against that version (and the fixtures re-captured). */
55
+ readonly verifiedAgainst: string;
56
+ /** Where the latest published version is found, so the update pipeline can
57
+ * detect a new release WITHOUT installing the CLI or running inference.
58
+ * `npm` is a pure registry query (credential-free, CI-friendly);
59
+ * `installed` harnesses (muse ships a shell script, not an npm package)
60
+ * have no registry to poll, so the check falls back to the locally
61
+ * installed `<bin> --version` and is skipped where the CLI is absent. */
62
+ readonly versionSource:
63
+ | { readonly kind: "npm"; readonly package: string }
64
+ | { readonly kind: "installed" };
65
+ /** Headless one-turn launch shape. `promptStyle: "positional"` means the
66
+ * prompt travels as a bare argv entry (ordering constraints apply).
67
+ * `streamFlags` is the output flag set a headless turn launches with so
68
+ * its stream is parseable and token-granular where the harness supports
69
+ * it - the v1 stall-watchdog scar was launching bare and then waiting for
70
+ * deltas the invocation could not emit. */
71
+ readonly launch: {
72
+ readonly baseFlags: readonly string[];
73
+ /** The subcommand words among baseFlags (codex/muse `exec`), declared
74
+ * explicitly - resume parsing anchors on them, and deriving them by
75
+ * filtering non-dash tokens would silently promote flag VALUES like
76
+ * `workspace-write` into subcommands. */
77
+ readonly subcommands: readonly string[];
78
+ readonly promptStyle: "positional";
79
+ readonly toolsFlag: string | null;
80
+ readonly streamFlags: readonly string[];
81
+ /** The flag that pins a caller-assigned id at LAUNCH (spawn-time
82
+ * assignment; the execution layer consumes it), or null when the
83
+ * harness mints its own. */
84
+ readonly idFlag: string | null;
85
+ };
86
+ /** How a named session id is resumed. `flag` style: `<bin> <flag> <id>`;
87
+ * `positional` style: `<bin> resume <id>` (muse) - the resume token must
88
+ * sit at argv position 1. `idShape` is the shape of the harness's own ids;
89
+ * parseResumeCommand refuses commands where an id-shaped token appears
90
+ * anywhere else (the v1 first-UUID-wins scar: a UUID inside quoted prompt
91
+ * text was returned as the session id, and resuming it started a stranger). */
92
+ readonly resume: {
93
+ readonly style: "flag" | "positional";
94
+ readonly flag: string;
95
+ readonly aliases: readonly string[];
96
+ readonly idShape: RegExp;
97
+ /** Flags valid in the RESUME grammar - resume argv never inherits
98
+ * launch flags, because subcommand grammars differ (codex exec resume
99
+ * rejects --sandbox). */
100
+ readonly extraFlags: readonly string[];
101
+ /** A parse-only positional spelling (muse's interactive `muse resume
102
+ * <id>`) recognized when pasted, but never built - the builder uses
103
+ * `style`/`flag`. */
104
+ readonly positionalParseWord?: string;
105
+ /** What resuming a NONEXISTENT id does (verified live): "error" -
106
+ * claude/codex refuse an unknown session; "create" - pi/muse treat the
107
+ * id as create-if-missing and silently start a FRESH session under it.
108
+ * The protocol layer must know this: a consumer resuming a session it
109
+ * believes exists gets a blank session, not an error, on a "create"
110
+ * harness. */
111
+ readonly onMissing: "error" | "create";
112
+ };
113
+ /** Persistent headless session support: the exact flag set that opens one
114
+ * lucid-owned process serving many turns, or null when the harness has no
115
+ * such mode. `idFlag` pins the caller-assigned session identity. */
116
+ readonly sessionMode: {
117
+ readonly flags: readonly string[];
118
+ readonly idFlag: string;
119
+ readonly input: SessionInputContract;
120
+ } | null;
121
+ /** Streaming is a property of the INVOCATION, not the harness: each pin
122
+ * names the flag set that unlocks a granularity, checked in order, first
123
+ * fully-satisfied pin wins; an argv satisfying none gets `floor`. This
124
+ * carries claude (token under stream-json), codex (message under --json,
125
+ * none bare), and muse/pi in one shape - a single conditional tier could
126
+ * not. `flagAliases` maps alternate spellings onto pin members. */
127
+ readonly output: {
128
+ readonly pins: ReadonlyArray<{
129
+ readonly flags: readonly string[];
130
+ readonly granularity: StreamingGranularity;
131
+ }>;
132
+ readonly floor: StreamingGranularity;
133
+ readonly flagAliases: Readonly<Record<string, string>>;
134
+ };
135
+ /** Where native identity appears in the stream, and who mints it. Every
136
+ * key/value pair in `announce.match` must match the raw event; claude:
137
+ * {type: "system", subtype: "init"}, re-emitted at every turn start with
138
+ * the same value (A-001) - consumers dedupe via decodeIdentity. */
139
+ readonly identity: {
140
+ readonly authority: "caller-assigned" | "harness-minted";
141
+ /** `idField` is a dot-path (muse nests its id at `stream.id`); an empty
142
+ * `match` means "any record carrying the id path". */
143
+ readonly announce: {
144
+ readonly match: Readonly<Record<string, string>>;
145
+ readonly idField: string;
146
+ };
147
+ };
148
+ /** "Stopped on a limit" vs crash vs clean exit: the harness's own wall
149
+ * phrasings. First match wins per line. */
150
+ readonly limitMatchers: ReadonlyArray<readonly [RegExp, LimitCode]>;
151
+ /** Auth-wall phrasings, same scan discipline as limitMatchers. */
152
+ readonly authMatchers: ReadonlyArray<readonly [RegExp, AuthFailureKind]>;
153
+ /** The "run unattended without stops" flag, or null when the harness has
154
+ * no such mode. */
155
+ readonly autonomy: { readonly flag: string } | null;
156
+ /** The harness's own model-id spellings, alias map, and effort ladder.
157
+ * Curated baseline - pi's registry is runtime-extensible, so validation
158
+ * against this vocabulary is a default, not a final word (D-008).
159
+ * `effortFlag` is null where effort is not a launch-time flag (claude:
160
+ * effort is an in-session command, not argv). */
161
+ readonly vocabulary: {
162
+ readonly modelFlag: string;
163
+ readonly models: readonly string[];
164
+ readonly aliases: Readonly<Record<string, string>>;
165
+ readonly efforts: readonly string[];
166
+ /** Per-model effort ladders where the harness constrains them (codex:
167
+ * gpt-5.5 tops out at high; gpt-5.6-* starts at medium). Falls back to
168
+ * the harness-wide `efforts`. */
169
+ readonly effortsByModel?: Readonly<Record<string, readonly string[]>>;
170
+ readonly effortFlag: string | null;
171
+ /** D-008: an extensible vocabulary (pi) accepts clean unknown model
172
+ * selectors at argv time; capability claims for them degrade to
173
+ * unknown until runtime verification. */
174
+ readonly extensible: boolean;
175
+ };
176
+ /** Where the harness files sessions/transcripts, as a template over
177
+ * {home}, {cwdSlug} and {sessionId}. Slugging rule is per-harness. */
178
+ readonly store: {
179
+ readonly template: string;
180
+ /** claude: '/', '.' -> '-'; pi: '/' -> '-' wrapped in leading/trailing
181
+ * dashes, dots preserved. */
182
+ readonly cwdSlug: "dash-separators" | "pi-dash-wrapped" | "verbatim";
183
+ };
184
+ /** How the harness exposes context-window usage; the interpretation layer
185
+ * surfaces it as a `context` HarnessEvent. */
186
+ readonly contextHook: {
187
+ readonly object: string;
188
+ readonly usedPctField: string;
189
+ } | null;
190
+ /** Resume-most-recent support (codex --last), or null. The race it opens
191
+ * is owned by the corroboration ranking in interpretation. */
192
+ readonly resumeLast: { readonly flag: string } | null;
193
+ /** Provider/model-route flag where present (pi --provider). */
194
+ readonly provider: { readonly flag: string } | null;
195
+ /** Whether backgrounded headless calls must have stdin closed (pi hangs
196
+ * without `< /dev/null`). */
197
+ readonly stdin: "inherit" | "close-required";
198
+ /** Flags that disable instruction-file/skill/MCP auto-discovery, in the
199
+ * spelling the harness accepts. */
200
+ readonly discoveryDisableFlags: readonly string[];
201
+ /** Presence recognition: how an interactive process for a session id shows
202
+ * up in a process listing. `headlessMarkers` mark a process as headless
203
+ * (not interactive presence). Known blind spot, inherent to argv matching:
204
+ * an interactive session carrying no id in argv (`claude --continue`) is
205
+ * invisible here - presence corroborates, it never proves. */
206
+ readonly presence: {
207
+ readonly headlessMarkers: readonly string[];
208
+ };
209
+ /** Curated capability baseline per mode (D-008: a default, never a final
210
+ * word - runtime verification upgrades source/confidence at attach). */
211
+ readonly capabilities: {
212
+ readonly vision: boolean;
213
+ readonly images: boolean;
214
+ readonly streamingByMode: Readonly<Record<HarnessMode, StreamingGranularity>>;
215
+ readonly session: boolean;
216
+ };
217
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Knowledge layer: owns harness descriptors as pure data.
3
+ *
4
+ * Each supported CLI (claude, codex, pi, muse) is described by an immutable
5
+ * descriptor capturing its argv shapes, identity and store locations, flag
6
+ * sets, and capability claims. The point of this module is to keep every fact
7
+ * about a harness in one declarative place as data, rather than scattered
8
+ * through branching logic. It is intentionally NOT responsible for
9
+ * interpreting descriptors or executing processes - those live in the
10
+ * interpretation and execution layers.
11
+ */
12
+ export { claudeCode } from "./claude-code.js";
13
+ export { codexCli } from "./codex.js";
14
+ export * from "./descriptor.js";
15
+ export { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
16
+ export { museCode } from "./muse.js";
17
+ export {
18
+ type DescriptorSet,
19
+ defaultDescriptors,
20
+ OverrideRefusalError,
21
+ parseOverrides,
22
+ } from "./overrides.js";
23
+ export { piCli } from "./pi.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Wall phrasings every harness shares. v1 applied every pattern to every
3
+ * harness deliberately - a muse turn dying on "You've hit your usage limit"
4
+ * or a provider 401 must classify no matter which CLI printed it. Each
5
+ * descriptor spreads these after its harness-specific phrasings.
6
+ */
7
+ import type { AuthFailureKind, LimitCode } from "./descriptor.js";
8
+
9
+ export const SHARED_LIMIT_MATCHERS: ReadonlyArray<readonly [RegExp, LimitCode]> = [
10
+ [/you'?ve hit your usage limit/i, "usage-limit"],
11
+ [/usage limit (?:reached|exceeded)/i, "usage-limit"],
12
+ [/purchase more credits|insufficient credits|out of credits/i, "credits"],
13
+ [/resource_exhausted|quota exceeded|exceeded your current quota/i, "quota"],
14
+ ];
15
+
16
+ export const SHARED_AUTH_MATCHERS: ReadonlyArray<readonly [RegExp, AuthFailureKind]> = [
17
+ [/401 unauthorized/i, "expired"],
18
+ [/invalid api key/i, "invalid-key"],
19
+ ];
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The muse descriptor: facts about the `muse` CLI as data, verified against
3
+ * Muse Code 0.1.0 and lucid v1's registry. Descriptor groundwork only
4
+ * (D-003). The v1 scars this encodes: headless re-entry is `muse exec
5
+ * --session-id <id>` (the positional `muse resume <id>` is the INTERACTIVE
6
+ * picker - recognized when pasted, never built), and `muse exec` exits 0
7
+ * even when the work inside failed (verification always reruns the
8
+ * project's own checks).
9
+ */
10
+ import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
11
+ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
12
+
13
+ export const museCode: HarnessDescriptor = deepFreeze({
14
+ name: "muse",
15
+ bin: "muse",
16
+ verifiedAgainst: "0.1.0",
17
+ versionSource: { kind: "installed" },
18
+ launch: {
19
+ // exec --json emits the payload_type/stream records the runner decodes
20
+ // (verified 0.1.0); bare exec streams human text.
21
+ baseFlags: ["exec", "--json"],
22
+ subcommands: ["exec"],
23
+ promptStyle: "positional",
24
+ toolsFlag: null,
25
+ streamFlags: [],
26
+ idFlag: "--session-id",
27
+ },
28
+ resume: {
29
+ // Headless re-entry: the same --session-id on exec (v1 registry). The
30
+ // interactive `muse resume <id>` spelling is parse-only.
31
+ style: "flag",
32
+ flag: "--session-id",
33
+ aliases: [],
34
+ idShape: UUID_SHAPE,
35
+ onMissing: "create",
36
+ // Structured output on resume too (see launch baseFlags).
37
+ extraFlags: ["--json"],
38
+ positionalParseWord: "resume",
39
+ },
40
+ sessionMode: null,
41
+ output: {
42
+ // muse exec --json emits payload.kind run_output_delta text chunks
43
+ // (verified 0.1.0), so this invocation is token-granular.
44
+ pins: [{ flags: ["--json"], granularity: "token" }],
45
+ floor: "none",
46
+ flagAliases: {},
47
+ },
48
+ identity: {
49
+ authority: "caller-assigned",
50
+ // Verified on 0.1.0 (--provider echo --json): records carry
51
+ // payload_type discriminators, no top-level type; the session id lives
52
+ // nested at stream.id - so match any record and read the path.
53
+ announce: { match: {}, idField: "stream.id" },
54
+ },
55
+ limitMatchers: [...SHARED_LIMIT_MATCHERS],
56
+ authMatchers: [...SHARED_AUTH_MATCHERS],
57
+ autonomy: { flag: "--yolo" },
58
+ vocabulary: {
59
+ modelFlag: "--model",
60
+ models: ["muse-spark-1.2-contributor", "muse-spark-1.2", "muse-spark-1.1"],
61
+ aliases: {},
62
+ efforts: ["none", "minimal", "low", "medium", "high", "xhigh", "ultra"],
63
+ effortFlag: "--reasoning-effort",
64
+ extensible: false,
65
+ },
66
+ store: {
67
+ // ~/.local/share/muse/sessions/YYYY/MM/DD/{id}/session.jsonl (index at
68
+ // session-index.db) - the template names the sessions root; the
69
+ // execution layer resolves the dated session file.
70
+ template: "{home}/.local/share/muse/sessions",
71
+ cwdSlug: "verbatim",
72
+ },
73
+ contextHook: null,
74
+ // `muse resume --last` exists (muse resume --help).
75
+ resumeLast: { flag: "--last" },
76
+ provider: null,
77
+ stdin: "inherit",
78
+ discoveryDisableFlags: [],
79
+ presence: {
80
+ headlessMarkers: ["exec"],
81
+ },
82
+ capabilities: {
83
+ // images: muse exec exposes --image <PATH>; kept false pending runtime
84
+ // verification (curated claims stay conservative).
85
+ vision: false,
86
+ images: false,
87
+ streamingByMode: {
88
+ "headless-turn": "token",
89
+ "headless-session": "none",
90
+ interactive: "none",
91
+ },
92
+ session: false,
93
+ },
94
+ });