litcodex-ai 0.3.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/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/litcodex.js +12 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +183 -0
- package/dist/config-migration/backup.d.ts +2 -0
- package/dist/config-migration/backup.js +42 -0
- package/dist/config-migration/catalog.d.ts +22 -0
- package/dist/config-migration/catalog.js +99 -0
- package/dist/config-migration/cli.d.ts +14 -0
- package/dist/config-migration/cli.js +85 -0
- package/dist/config-migration/config-paths.d.ts +4 -0
- package/dist/config-migration/config-paths.js +64 -0
- package/dist/config-migration/errors.d.ts +11 -0
- package/dist/config-migration/errors.js +28 -0
- package/dist/config-migration/index.d.ts +44 -0
- package/dist/config-migration/index.js +210 -0
- package/dist/config-migration/multi-agent-v2-guard.d.ts +2 -0
- package/dist/config-migration/multi-agent-v2-guard.js +106 -0
- package/dist/config-migration/root-settings.d.ts +6 -0
- package/dist/config-migration/root-settings.js +104 -0
- package/dist/config-migration/state.d.ts +16 -0
- package/dist/config-migration/state.js +40 -0
- package/dist/config-migration/toml-shape.d.ts +8 -0
- package/dist/config-migration/toml-shape.js +107 -0
- package/dist/install/codex.d.ts +34 -0
- package/dist/install/codex.js +94 -0
- package/dist/install/doctor.d.ts +12 -0
- package/dist/install/doctor.js +83 -0
- package/dist/install/errors.d.ts +19 -0
- package/dist/install/errors.js +43 -0
- package/dist/install/execute.d.ts +39 -0
- package/dist/install/execute.js +193 -0
- package/dist/install/index.d.ts +19 -0
- package/dist/install/index.js +193 -0
- package/dist/install/marketplace.d.ts +5 -0
- package/dist/install/marketplace.js +10 -0
- package/dist/install/plan.d.ts +3 -0
- package/dist/install/plan.js +54 -0
- package/dist/install/render-plan.d.ts +3 -0
- package/dist/install/render-plan.js +10 -0
- package/dist/install/types.d.ts +45 -0
- package/dist/install/types.js +5 -0
- package/model-catalog.json +31 -0
- package/node_modules/@litcodex/lit-loop/CHANGELOG.md +19 -0
- package/node_modules/@litcodex/lit-loop/LICENSE +21 -0
- package/node_modules/@litcodex/lit-loop/NOTICE +8 -0
- package/node_modules/@litcodex/lit-loop/README.md +37 -0
- package/node_modules/@litcodex/lit-loop/agents/litcodex-explorer.toml +75 -0
- package/node_modules/@litcodex/lit-loop/agents/litcodex-librarian.toml +98 -0
- package/node_modules/@litcodex/lit-loop/agents/litcodex-litwork-reviewer.toml +21 -0
- package/node_modules/@litcodex/lit-loop/agents/litcodex-metis.toml +64 -0
- package/node_modules/@litcodex/lit-loop/agents/litcodex-momus.toml +68 -0
- package/node_modules/@litcodex/lit-loop/agents/litcodex-plan.toml +163 -0
- package/node_modules/@litcodex/lit-loop/directive.md +85 -0
- package/node_modules/@litcodex/lit-loop/directives/lit-plan.md +286 -0
- package/node_modules/@litcodex/lit-loop/directives/litgoal.md +103 -0
- package/node_modules/@litcodex/lit-loop/directives/litwork.md +363 -0
- package/node_modules/@litcodex/lit-loop/dist/_scaffold.d.ts +1 -0
- package/node_modules/@litcodex/lit-loop/dist/_scaffold.js +3 -0
- package/node_modules/@litcodex/lit-loop/dist/cli.d.ts +6 -0
- package/node_modules/@litcodex/lit-loop/dist/cli.js +44 -0
- package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.d.ts +18 -0
- package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.js +94 -0
- package/node_modules/@litcodex/lit-loop/dist/codex-hook.d.ts +38 -0
- package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +126 -0
- package/node_modules/@litcodex/lit-loop/dist/directive.d.ts +35 -0
- package/node_modules/@litcodex/lit-loop/dist/directive.js +80 -0
- package/node_modules/@litcodex/lit-loop/dist/goal-status.d.ts +12 -0
- package/node_modules/@litcodex/lit-loop/dist/goal-status.js +25 -0
- package/node_modules/@litcodex/lit-loop/dist/guards.d.ts +73 -0
- package/node_modules/@litcodex/lit-loop/dist/guards.js +215 -0
- package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +17 -0
- package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +94 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-cli.d.ts +19 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-cli.js +106 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor-render.d.ts +7 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor-render.js +39 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor-types.d.ts +52 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor-types.js +7 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor.d.ts +21 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor.js +283 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-errors.d.ts +15 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-errors.js +43 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-handlers.d.ts +18 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-handlers.js +311 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-model.d.ts +51 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-model.js +165 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-stdout.d.ts +6 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-stdout.js +11 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-types.d.ts +26 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-types.js +8 -0
- package/node_modules/@litcodex/lit-loop/dist/markers.d.ts +9 -0
- package/node_modules/@litcodex/lit-loop/dist/markers.js +14 -0
- package/node_modules/@litcodex/lit-loop/dist/modes.d.ts +15 -0
- package/node_modules/@litcodex/lit-loop/dist/modes.js +56 -0
- package/node_modules/@litcodex/lit-loop/dist/state-paths.d.ts +41 -0
- package/node_modules/@litcodex/lit-loop/dist/state-paths.js +111 -0
- package/node_modules/@litcodex/lit-loop/dist/state-store.d.ts +39 -0
- package/node_modules/@litcodex/lit-loop/dist/state-store.js +419 -0
- package/node_modules/@litcodex/lit-loop/dist/state-types.d.ts +90 -0
- package/node_modules/@litcodex/lit-loop/dist/state-types.js +61 -0
- package/node_modules/@litcodex/lit-loop/dist/trigger.d.ts +54 -0
- package/node_modules/@litcodex/lit-loop/dist/trigger.js +75 -0
- package/node_modules/@litcodex/lit-loop/package.json +27 -0
- package/package.json +30 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/codex-hook.ts — M06 pure hook engine (A2 §2.4, A3 C3/C12).
|
|
2
|
+
//
|
|
3
|
+
// Takes an already-parsed `unknown` (the JSON.parse'd stdin value) and returns a structured
|
|
4
|
+
// HookDecision. NO process / stdin / stdout / exit lives here — that is hook-cli.ts. The engine is
|
|
5
|
+
// total over `unknown` and NEVER throws on a structurally-valid event.
|
|
6
|
+
//
|
|
7
|
+
// Activation policy is delegated to the SINGLE guard entry point
|
|
8
|
+
// `shouldSuppressLitLoopInjection(prompt, transcriptPath)` from ./guards.js (A3 C3): the engine
|
|
9
|
+
// branches only on its `{suppress, reason}` result and imports NOTHING from the trigger module
|
|
10
|
+
// (guard 0 `not-a-trigger` already covers lexical trigger detection). The engine never reads the
|
|
11
|
+
// transcript itself and never slices the prompt by any offset (A3 A6).
|
|
12
|
+
//
|
|
13
|
+
// Casing (A3 C12): the input type-guard accepts BOTH the snake_case wire key `hook_event_name`
|
|
14
|
+
// (primary) AND the camelCase `hookEventName` (fallback). The emitted envelope is ALWAYS camelCase
|
|
15
|
+
// (`hookSpecificOutput.hookEventName`).
|
|
16
|
+
//
|
|
17
|
+
// On activation the payload is the trusted bundled directive, loaded behind a FAIL-SILENT boundary:
|
|
18
|
+
// a directive-load error resolves to "" (→ noop), so a missing dist/directive.md degrades the hook
|
|
19
|
+
// to a silent no-op + exit 0 rather than throwing into the Codex host.
|
|
20
|
+
import { loadDirectiveFrom } from "./directive.js";
|
|
21
|
+
import { shouldSuppressInjection } from "./guards.js";
|
|
22
|
+
import { modeForToken } from "./modes.js";
|
|
23
|
+
import { matchLitTrigger } from "./trigger.js";
|
|
24
|
+
const HOOK_EVENT_NAME = "UserPromptSubmit";
|
|
25
|
+
/** Narrow to a plain (non-null, non-array) record. */
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
/** Read either casing of the event-name key (snake wins on conflict, A3 C12). */
|
|
30
|
+
function readEventName(record) {
|
|
31
|
+
if (Object.hasOwn(record, "hook_event_name")) {
|
|
32
|
+
return record["hook_event_name"];
|
|
33
|
+
}
|
|
34
|
+
return record["hookEventName"];
|
|
35
|
+
}
|
|
36
|
+
/** Read either casing of the transcript-path key (snake wins on conflict). */
|
|
37
|
+
function readTranscriptPath(record) {
|
|
38
|
+
if (Object.hasOwn(record, "transcript_path")) {
|
|
39
|
+
return record["transcript_path"];
|
|
40
|
+
}
|
|
41
|
+
if (Object.hasOwn(record, "transcriptPath")) {
|
|
42
|
+
return record["transcriptPath"];
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function isTranscriptPathValue(value) {
|
|
47
|
+
return value === undefined || value === null || typeof value === "string";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Type-guard. True iff `value` is a record whose accepted event-name key (snake `hook_event_name`
|
|
51
|
+
* primary, camel `hookEventName` fallback) === "UserPromptSubmit", `prompt` is a string, and the
|
|
52
|
+
* transcript-path key (either casing) is undefined | null | string (A3 C12). This is a runtime
|
|
53
|
+
* accept-set, not a structural type assert, so a camelCase-keyed record also returns true.
|
|
54
|
+
*/
|
|
55
|
+
export function isLitUserPromptSubmitInput(value) {
|
|
56
|
+
if (!isRecord(value)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (readEventName(value) !== HOOK_EVENT_NAME) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value["prompt"] !== "string") {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return isTranscriptPathValue(readTranscriptPath(value));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wraps directive text in the Codex output envelope. Returns "" if the normalized context is empty
|
|
69
|
+
* (caller treats "" as noop). Output is a single-line JSON + trailing "\n":
|
|
70
|
+
* {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":<ctx>}}\n
|
|
71
|
+
*/
|
|
72
|
+
export function formatAdditionalContextOutput(additionalContext) {
|
|
73
|
+
const normalized = additionalContext.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
74
|
+
if (normalized.length === 0) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
const output = {
|
|
78
|
+
hookSpecificOutput: {
|
|
79
|
+
hookEventName: HOOK_EVENT_NAME,
|
|
80
|
+
additionalContext: normalized,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return `${JSON.stringify(output)}\n`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Load the matched mode's directive behind a fail-silent boundary: any loader error (e.g. a missing
|
|
87
|
+
* directives/<mode>.md or a marker-wrapper mismatch) resolves to "" so the hook degrades to a silent
|
|
88
|
+
* no-op instead of throwing into the Codex host.
|
|
89
|
+
*/
|
|
90
|
+
function loadDirectiveForModeFailSilent(mode) {
|
|
91
|
+
try {
|
|
92
|
+
return loadDirectiveFrom(mode.directivePath, mode.openMarker, mode.closeMarker);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Pure decision function (multi-mode router). Takes an already-parsed value (NOT a raw string).
|
|
100
|
+
* Total over `unknown`; NEVER throws. Flow: validate shape → match the lit-family trigger token
|
|
101
|
+
* (guard 0) → resolve the mode → run guards 1-3 against the MATCHED mode's marker (RC1) → load that
|
|
102
|
+
* mode's directive fail-silent. Returns { kind:"noop" } for a wrong-shape value, no trigger, a guard
|
|
103
|
+
* veto, or a fail-silent empty directive; { kind:"inject", stdout } only on a real activation.
|
|
104
|
+
*/
|
|
105
|
+
export function runUserPromptSubmitHook(input) {
|
|
106
|
+
if (!isLitUserPromptSubmitInput(input)) {
|
|
107
|
+
return { kind: "noop" };
|
|
108
|
+
}
|
|
109
|
+
// `input` is a narrowed record here; re-read the transcript key (either casing) as a raw value.
|
|
110
|
+
const transcriptPath = readTranscriptPath(input);
|
|
111
|
+
// Guard 0 / routing: which bounded lit-family token matched → which mode + directive.
|
|
112
|
+
const match = matchLitTrigger(input.prompt);
|
|
113
|
+
if (match === null) {
|
|
114
|
+
return { kind: "noop" };
|
|
115
|
+
}
|
|
116
|
+
const mode = modeForToken(match.token);
|
|
117
|
+
const decision = shouldSuppressInjection(input.prompt, transcriptPath, mode.openMarker);
|
|
118
|
+
if (decision.suppress) {
|
|
119
|
+
return { kind: "noop" };
|
|
120
|
+
}
|
|
121
|
+
const stdout = formatAdditionalContextOutput(loadDirectiveForModeFailSilent(mode));
|
|
122
|
+
if (stdout === "") {
|
|
123
|
+
return { kind: "noop" };
|
|
124
|
+
}
|
|
125
|
+
return { kind: "inject", stdout };
|
|
126
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { LIT_LOOP_DIRECTIVE_MARKER } from "./markers.js";
|
|
2
|
+
/** Machine-readable codes for the three load-failure classes (ordered: unreadable → empty → marker). */
|
|
3
|
+
export type LitLoopDirectiveErrorCode = "LIT_LOOP_DIRECTIVE_UNREADABLE" | "LIT_LOOP_DIRECTIVE_EMPTY" | "LIT_LOOP_DIRECTIVE_MARKER_MISSING";
|
|
4
|
+
/** Typed error carrying a machine-readable code and the absolute path the loader attempted. */
|
|
5
|
+
export declare class LitLoopDirectiveError extends Error {
|
|
6
|
+
readonly code: LitLoopDirectiveErrorCode;
|
|
7
|
+
readonly resolvedPath: string;
|
|
8
|
+
constructor(code: LitLoopDirectiveErrorCode, message: string, resolvedPath: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generic (RC2): read + normalize + validate ANY mode's directive at an explicit path against an
|
|
12
|
+
* explicit `openMarker`/`closeMarker` pair. The multi-mode router (codex-hook → modes) uses this to
|
|
13
|
+
* load litwork / lit-plan / litgoal directives; `loadLitLoopDirectiveFrom` is the lit-loop wrapper.
|
|
14
|
+
* Normalization: CRLF→LF, lone CR→LF, then trim(). Fail-loud with a typed error (codes are generic).
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadDirectiveFrom(resolvedPath: string, openMarker: string, closeMarker: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Read + normalize + validate the lit-loop directive at an explicit path. Test-only seam (S10
|
|
19
|
+
* §Test plan) so corrupt / CRLF / missing-file cases can be exercised without swapping the bundled
|
|
20
|
+
* file. Delegates to the generic `loadDirectiveFrom` with the lit-loop marker pair.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadLitLoopDirectiveFrom(resolvedPath: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Load the bundled directive.md from its resolved dist path. Re-readable on demand (tests /
|
|
25
|
+
* dev hot-reload); production consumers use the eagerly-computed `LIT_LOOP_DIRECTIVE` constant.
|
|
26
|
+
*
|
|
27
|
+
* @throws LitLoopDirectiveError code UNREADABLE / EMPTY / MARKER_MISSING.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadLitLoopDirective(): string;
|
|
30
|
+
/**
|
|
31
|
+
* The normalized directive text, read once at module-load time. Guaranteed to start with
|
|
32
|
+
* `<lit-loop-mode>`, end with `</lit-loop-mode>`, and contain no `\r`; non-empty; frozen for
|
|
33
|
+
* the process lifetime.
|
|
34
|
+
*/
|
|
35
|
+
export declare const LIT_LOOP_DIRECTIVE: string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/directive.ts — M10 lit-loop directive loader.
|
|
2
|
+
//
|
|
3
|
+
// Reads the bundled directive.md at module-load time, normalizes line endings + trims, and
|
|
4
|
+
// exports the normalized text plus the wrapper marker. This module owns content + loading only:
|
|
5
|
+
// it never parses prompts, emits hook JSON, applies guards, or persists loop state, and it
|
|
6
|
+
// imports NOTHING from trigger / guards / state-store / codex-hook (one-way: hook → directive).
|
|
7
|
+
//
|
|
8
|
+
// A3 C1: the marker is the single-source `markers.ts` value — re-exported here, never re-declared.
|
|
9
|
+
// A3 G6 (SUPERSEDES C8): the single authored `directive.md` lives at the COMPONENT ROOT (tracked,
|
|
10
|
+
// in package.json files[]). The loader resolves `../directive.md` relative to its own module
|
|
11
|
+
// so it lands on that one file from BOTH layouts: src/directive.ts → <component>/directive.md
|
|
12
|
+
// (dev / vitest src-tree) AND dist/directive.js → <pkg>/directive.md (published tarball,
|
|
13
|
+
// since files[] ships directive.md at the tarball root beside dist/). No build-copy, no
|
|
14
|
+
// src/dist mirror — so a clean checkout passes `npm test` BEFORE any `npm run build`.
|
|
15
|
+
// Resolution uses `fileURLToPath(new URL(...))` — the percent-decoding, Windows-safe form
|
|
16
|
+
// that survives space/#/Hangul in the repo path.
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { LIT_LOOP_DIRECTIVE_CLOSE, LIT_LOOP_DIRECTIVE_MARKER } from "./markers.js";
|
|
20
|
+
// Re-export the single-source marker (A3 C1) so consumers import it from directive.ts unchanged.
|
|
21
|
+
export { LIT_LOOP_DIRECTIVE_MARKER } from "./markers.js";
|
|
22
|
+
/** Typed error carrying a machine-readable code and the absolute path the loader attempted. */
|
|
23
|
+
export class LitLoopDirectiveError extends Error {
|
|
24
|
+
constructor(code, message, resolvedPath) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "LitLoopDirectiveError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.resolvedPath = resolvedPath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// A3 G6: directive.md is the authored component-root file, one level above this module's dir
|
|
32
|
+
// (above src/ in the dev tree; above dist/ in the published tarball — files[] ships it at root).
|
|
33
|
+
const DIRECTIVE_PATH = fileURLToPath(new URL("../directive.md", import.meta.url));
|
|
34
|
+
/**
|
|
35
|
+
* Generic (RC2): read + normalize + validate ANY mode's directive at an explicit path against an
|
|
36
|
+
* explicit `openMarker`/`closeMarker` pair. The multi-mode router (codex-hook → modes) uses this to
|
|
37
|
+
* load litwork / lit-plan / litgoal directives; `loadLitLoopDirectiveFrom` is the lit-loop wrapper.
|
|
38
|
+
* Normalization: CRLF→LF, lone CR→LF, then trim(). Fail-loud with a typed error (codes are generic).
|
|
39
|
+
*/
|
|
40
|
+
export function loadDirectiveFrom(resolvedPath, openMarker, closeMarker) {
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = readFileSync(resolvedPath, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
47
|
+
throw new LitLoopDirectiveError("LIT_LOOP_DIRECTIVE_UNREADABLE", `directive: unreadable at ${resolvedPath}: ${detail}`, resolvedPath);
|
|
48
|
+
}
|
|
49
|
+
const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
50
|
+
if (normalized.length === 0) {
|
|
51
|
+
throw new LitLoopDirectiveError("LIT_LOOP_DIRECTIVE_EMPTY", `directive: empty after normalization at ${resolvedPath}`, resolvedPath);
|
|
52
|
+
}
|
|
53
|
+
if (!normalized.startsWith(openMarker) || !normalized.endsWith(closeMarker)) {
|
|
54
|
+
throw new LitLoopDirectiveError("LIT_LOOP_DIRECTIVE_MARKER_MISSING", `directive: missing ${openMarker} … ${closeMarker} wrapper at ${resolvedPath}`, resolvedPath);
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read + normalize + validate the lit-loop directive at an explicit path. Test-only seam (S10
|
|
60
|
+
* §Test plan) so corrupt / CRLF / missing-file cases can be exercised without swapping the bundled
|
|
61
|
+
* file. Delegates to the generic `loadDirectiveFrom` with the lit-loop marker pair.
|
|
62
|
+
*/
|
|
63
|
+
export function loadLitLoopDirectiveFrom(resolvedPath) {
|
|
64
|
+
return loadDirectiveFrom(resolvedPath, LIT_LOOP_DIRECTIVE_MARKER, LIT_LOOP_DIRECTIVE_CLOSE);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load the bundled directive.md from its resolved dist path. Re-readable on demand (tests /
|
|
68
|
+
* dev hot-reload); production consumers use the eagerly-computed `LIT_LOOP_DIRECTIVE` constant.
|
|
69
|
+
*
|
|
70
|
+
* @throws LitLoopDirectiveError code UNREADABLE / EMPTY / MARKER_MISSING.
|
|
71
|
+
*/
|
|
72
|
+
export function loadLitLoopDirective() {
|
|
73
|
+
return loadLitLoopDirectiveFrom(DIRECTIVE_PATH);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The normalized directive text, read once at module-load time. Guaranteed to start with
|
|
77
|
+
* `<lit-loop-mode>`, end with `</lit-loop-mode>`, and contain no `\r`; non-empty; frozen for
|
|
78
|
+
* the process lifetime.
|
|
79
|
+
*/
|
|
80
|
+
export const LIT_LOOP_DIRECTIVE = loadLitLoopDirective();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LoopGoal, LoopPlan } from "./loop-types.js";
|
|
2
|
+
/** The Codex goal objective for a given lit-loop goal (per-story: one Codex goal per goal). */
|
|
3
|
+
export declare function expectedCodexObjective(goal: LoopGoal): string;
|
|
4
|
+
/** True when the goal has at least one criterion and every criterion is `pass`. */
|
|
5
|
+
export declare function hasAllCriteriaPass(goal: LoopGoal): boolean;
|
|
6
|
+
/** True when every goal in the plan is `complete`. */
|
|
7
|
+
export declare function isLitLoopDone(plan: LoopPlan): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True when the given goal is NOT complete AND every OTHER goal IS complete — meaning completing
|
|
10
|
+
* this goal would finish the entire plan.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isFinalRunCompletionCandidate(plan: LoopPlan, goal: LoopGoal): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/goal-status.ts — pure goal-status helpers for Codex native-goal integration.
|
|
2
|
+
//
|
|
3
|
+
// Predicates and objective resolver for the lit-loop ↔ Codex `/goal` handoff. All functions are
|
|
4
|
+
// PURE — no I/O, no store, no clock. Imports only loop-types (which re-exports state-types).
|
|
5
|
+
/** The Codex goal objective for a given lit-loop goal (per-story: one Codex goal per goal). */
|
|
6
|
+
export function expectedCodexObjective(goal) {
|
|
7
|
+
return goal.objective;
|
|
8
|
+
}
|
|
9
|
+
/** True when the goal has at least one criterion and every criterion is `pass`. */
|
|
10
|
+
export function hasAllCriteriaPass(goal) {
|
|
11
|
+
return goal.successCriteria.length > 0 && goal.successCriteria.every((c) => c.status === "pass");
|
|
12
|
+
}
|
|
13
|
+
/** True when every goal in the plan is `complete`. */
|
|
14
|
+
export function isLitLoopDone(plan) {
|
|
15
|
+
return plan.goals.every((g) => g.status === "complete");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when the given goal is NOT complete AND every OTHER goal IS complete — meaning completing
|
|
19
|
+
* this goal would finish the entire plan.
|
|
20
|
+
*/
|
|
21
|
+
export function isFinalRunCompletionCandidate(plan, goal) {
|
|
22
|
+
if (goal.status === "complete")
|
|
23
|
+
return false;
|
|
24
|
+
return plan.goals.every((g) => g.id === goal.id || g.status === "complete");
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export { LIT_LOOP_DIRECTIVE_MARKER } from "./markers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Context-pressure / compaction / recovery markers. Lowercase substrings; matching is
|
|
4
|
+
* case-insensitive (input is lowercased before .includes). VERBATIM and ORDER-STABLE — the single
|
|
5
|
+
* source of truth (A3 §E): any module needing context-pressure detection MUST import this array,
|
|
6
|
+
* never copy it. Marker 5 retains the literal token `codex` (the host runtime, NOT a legacy brand —
|
|
7
|
+
* A3 C16) and the literal U+0027 apostrophe in `model's`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONTEXT_PRESSURE_MARKERS: readonly ["context compacted", "context_length_exceeded", "skill descriptions were shortened", "context_too_large", "codex ran out of room in the model's context window", "your input exceeds the context window", "long threads and multiple compactions"];
|
|
10
|
+
/** Tail window (bytes) read for the guard-2 directive-dedupe scan. Exactly 512000. */
|
|
11
|
+
export declare const TRANSCRIPT_SEARCH_BYTES: 512000;
|
|
12
|
+
/**
|
|
13
|
+
* Per-end byte budget for the guard-3 (context-pressure-transcript) read. Guard 3 reads at most the
|
|
14
|
+
* first and last GUARD3_WINDOW_BYTES of the transcript (whole file when smaller than 2×). Exactly
|
|
15
|
+
* 1 MiB per end so worst-case guard-3 work is ≤ 2 MiB of fixed-string scanning (A3 addendum §A).
|
|
16
|
+
* Distinct from TRANSCRIPT_SEARCH_BYTES (the guard-2 tail); the two MUST NOT be aliased.
|
|
17
|
+
*/
|
|
18
|
+
export declare const GUARD3_WINDOW_BYTES: 1048576;
|
|
19
|
+
/**
|
|
20
|
+
* The hook OUTPUT envelope event-name value, written to the transcript by M06 and matched by guard
|
|
21
|
+
* 2 (A3 addendum §D). camelCase, load-bearing; declared exactly once here so M06's emitter and this
|
|
22
|
+
* guard cannot drift. M06 imports this from ./guards.js (one-way: codex-hook -> guards).
|
|
23
|
+
*/
|
|
24
|
+
export declare const HOOK_OUTPUT_EVENT_NAME: "UserPromptSubmit";
|
|
25
|
+
/** Normalized transcript-path input shape accepted by the file-reading guards. */
|
|
26
|
+
export type GuardTranscriptInput = string | null | undefined;
|
|
27
|
+
/** Structured suppression decision returned to the M06 hook runner. */
|
|
28
|
+
export interface LitLoopGuardDecision {
|
|
29
|
+
/** true => the hook MUST emit "" (no directive); false => injection may proceed. */
|
|
30
|
+
readonly suppress: boolean;
|
|
31
|
+
/** Machine-readable reason. "none" only when suppress === false. */
|
|
32
|
+
readonly reason: "none" | "context-pressure-prompt" | "already-injected" | "context-pressure-transcript" | "not-a-trigger";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pure lexical test: does `text` contain ANY context-pressure marker? Lowercases `text`, then
|
|
36
|
+
* CONTEXT_PRESSURE_MARKERS.some(m => lower.includes(m)). No I/O, no state, idempotent.
|
|
37
|
+
* @throws TypeError if `text` is not a string.
|
|
38
|
+
*/
|
|
39
|
+
export declare function hasContextPressureMarker(text: string): boolean;
|
|
40
|
+
/** Alias used by the prompt-level guard (guard 1). Identical semantics to hasContextPressureMarker. */
|
|
41
|
+
export declare function isContextPressurePrompt(prompt: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Generic per-mode idempotency check (RC1). Reads the TAIL (last TRANSCRIPT_SEARCH_BYTES bytes) of
|
|
44
|
+
* the transcript, parses each JSONL line, and returns true iff some line is a prior HOOK OUTPUT
|
|
45
|
+
* envelope: parsed.hookSpecificOutput.hookEventName === HOOK_OUTPUT_EVENT_NAME AND
|
|
46
|
+
* typeof additionalContext === "string" AND additionalContext.includes(`marker`). A marker quoted
|
|
47
|
+
* inside a user/assistant content STRING (or a forged envelope embedded as text) is NOT a top-level
|
|
48
|
+
* envelope line and is skipped. FAIL-OPEN: any thrown Error => false. The multi-mode hook passes the
|
|
49
|
+
* MATCHED mode's open marker so re-injection is suppressed per mode (mode switching is allowed).
|
|
50
|
+
*/
|
|
51
|
+
export declare function transcriptHasDirectiveMarker(transcriptPath: GuardTranscriptInput, marker: string): boolean;
|
|
52
|
+
/** Lit-loop idempotency guard (guard 2) — the generic check bound to the lit-loop marker. */
|
|
53
|
+
export declare function transcriptHasLitLoopDirective(transcriptPath: GuardTranscriptInput): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Context-pressure transcript guard (guard 3). Reads a bounded head+tail window of the transcript
|
|
56
|
+
* (A3 addendum §A): the whole file when ≤ 2×GUARD3_WINDOW_BYTES, else the first + last
|
|
57
|
+
* GUARD3_WINDOW_BYTES joined by a "\n" separator (so a marker cannot be manufactured across the
|
|
58
|
+
* cut), then runs hasContextPressureMarker over it. Markers buried in nested JSON are caught by
|
|
59
|
+
* design. FAIL-OPEN: any thrown Error => false. null/undefined path => false.
|
|
60
|
+
*/
|
|
61
|
+
export declare function transcriptHasContextPressureMarker(transcriptPath: GuardTranscriptInput): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* THE single decision the M06 hook runner calls. Evaluates the four guards in fixed order and
|
|
64
|
+
* returns the first veto, else {suppress:false, reason:"none"}. Suppression dominates activation:
|
|
65
|
+
* guard 0 (not-a-trigger) runs first so the file-I/O guards only fire when a trigger is present;
|
|
66
|
+
* guard 1 (context-pressure-prompt) runs before the transcript guards so a recovery prompt that
|
|
67
|
+
* also contains `lit` is suppressed without any file read. Never throws for any string prompt +
|
|
68
|
+
* GuardTranscriptInput path (the only throw is the non-string prompt defensive guard).
|
|
69
|
+
* @throws TypeError only if `prompt` is not a string (M06 type-guards this upstream).
|
|
70
|
+
*/
|
|
71
|
+
export declare function shouldSuppressInjection(prompt: string, transcriptPath: GuardTranscriptInput, openMarker: string): LitLoopGuardDecision;
|
|
72
|
+
/** Lit-loop entry point (backward-compatible) — the generic decision bound to the lit-loop marker. */
|
|
73
|
+
export declare function shouldSuppressLitLoopInjection(prompt: string, transcriptPath: GuardTranscriptInput): LitLoopGuardDecision;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// src/guards.ts — M07 activation-policy guard layer.
|
|
2
|
+
//
|
|
3
|
+
// Sits between the M05 bounded trigger matcher (./trigger.js) and the M06 hook runner. M05 asks
|
|
4
|
+
// "does this prompt lexically contain a `lit` trigger?"; M07 asks "given the surrounding session
|
|
5
|
+
// state, should we actually inject the <lit-loop-mode> directive NOW?". It owns four suppression
|
|
6
|
+
// guards, evaluated in a fixed order (suppression dominates activation):
|
|
7
|
+
// 0. not a lit trigger -> suppress, "not-a-trigger"
|
|
8
|
+
// 1. prompt is a context-pressure msg -> suppress, "context-pressure-prompt"
|
|
9
|
+
// 2. transcript already hook-injected -> suppress, "already-injected"
|
|
10
|
+
// 3. transcript shows context pressure -> suppress, "context-pressure-transcript"
|
|
11
|
+
// else -> allow, "none"
|
|
12
|
+
//
|
|
13
|
+
// Detection is PURELY lexical/structural (fixed marker tables + a fixed JSON-envelope shape), so
|
|
14
|
+
// prompt prose cannot alter any guard outcome. Every file-reading guard is FAIL-OPEN: any thrown
|
|
15
|
+
// Error returns false (no suppression from that guard), never throwing — a missing, unreadable,
|
|
16
|
+
// oversized, or malformed transcript can never crash the Codex turn. This module reads transcript
|
|
17
|
+
// files but writes nothing and holds no mutable state.
|
|
18
|
+
//
|
|
19
|
+
// Dependency direction (A3 C9 flat layout; one-way: codex-hook -> guards -> trigger): imports ONLY
|
|
20
|
+
// ./trigger.js and ./markers.js; NEVER ./directive.js, ./state-store.js, or ./codex-hook.js
|
|
21
|
+
// (no-cycle invariant). The marker is the single-source markers.ts value, re-exported here
|
|
22
|
+
// (A3 C1) — never re-declared.
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { LIT_LOOP_DIRECTIVE_MARKER } from "./markers.js";
|
|
25
|
+
import { isLitTriggerPrompt } from "./trigger.js";
|
|
26
|
+
// Re-export the single-source directive marker (A3 C1) so the guards public surface is unchanged
|
|
27
|
+
// while markers.ts stays the only declaration site (guards never imports directive — no cycle).
|
|
28
|
+
export { LIT_LOOP_DIRECTIVE_MARKER } from "./markers.js";
|
|
29
|
+
/**
|
|
30
|
+
* Context-pressure / compaction / recovery markers. Lowercase substrings; matching is
|
|
31
|
+
* case-insensitive (input is lowercased before .includes). VERBATIM and ORDER-STABLE — the single
|
|
32
|
+
* source of truth (A3 §E): any module needing context-pressure detection MUST import this array,
|
|
33
|
+
* never copy it. Marker 5 retains the literal token `codex` (the host runtime, NOT a legacy brand —
|
|
34
|
+
* A3 C16) and the literal U+0027 apostrophe in `model's`.
|
|
35
|
+
*/
|
|
36
|
+
export const CONTEXT_PRESSURE_MARKERS = Object.freeze([
|
|
37
|
+
"context compacted",
|
|
38
|
+
"context_length_exceeded",
|
|
39
|
+
"skill descriptions were shortened",
|
|
40
|
+
"context_too_large",
|
|
41
|
+
"codex ran out of room in the model's context window",
|
|
42
|
+
"your input exceeds the context window",
|
|
43
|
+
"long threads and multiple compactions",
|
|
44
|
+
]);
|
|
45
|
+
/** Tail window (bytes) read for the guard-2 directive-dedupe scan. Exactly 512000. */
|
|
46
|
+
export const TRANSCRIPT_SEARCH_BYTES = 512000;
|
|
47
|
+
/**
|
|
48
|
+
* Per-end byte budget for the guard-3 (context-pressure-transcript) read. Guard 3 reads at most the
|
|
49
|
+
* first and last GUARD3_WINDOW_BYTES of the transcript (whole file when smaller than 2×). Exactly
|
|
50
|
+
* 1 MiB per end so worst-case guard-3 work is ≤ 2 MiB of fixed-string scanning (A3 addendum §A).
|
|
51
|
+
* Distinct from TRANSCRIPT_SEARCH_BYTES (the guard-2 tail); the two MUST NOT be aliased.
|
|
52
|
+
*/
|
|
53
|
+
export const GUARD3_WINDOW_BYTES = 1048576;
|
|
54
|
+
/**
|
|
55
|
+
* The hook OUTPUT envelope event-name value, written to the transcript by M06 and matched by guard
|
|
56
|
+
* 2 (A3 addendum §D). camelCase, load-bearing; declared exactly once here so M06's emitter and this
|
|
57
|
+
* guard cannot drift. M06 imports this from ./guards.js (one-way: codex-hook -> guards).
|
|
58
|
+
*/
|
|
59
|
+
export const HOOK_OUTPUT_EVENT_NAME = "UserPromptSubmit";
|
|
60
|
+
const PROMPT_NOT_A_STRING = "lit guard: prompt must be a string";
|
|
61
|
+
const TEXT_NOT_A_STRING = "lit guard: text must be a string";
|
|
62
|
+
// --- Internal helpers (not exported; semantics load-bearing) ----------------------------------
|
|
63
|
+
/** Narrow to a plain (non-null, non-array) record. */
|
|
64
|
+
function isRecord(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse one JSONL line. Empty/whitespace line -> null; JSON.parse Error -> null (skipped);
|
|
69
|
+
* a non-Error throw (rare) is rethrown so the caller's fail-open boundary handles only Errors.
|
|
70
|
+
*/
|
|
71
|
+
function parseJsonLine(line) {
|
|
72
|
+
if (line.trim().length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(line);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// --- Pure marker matcher (guard 1 primitive) ---------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Pure lexical test: does `text` contain ANY context-pressure marker? Lowercases `text`, then
|
|
88
|
+
* CONTEXT_PRESSURE_MARKERS.some(m => lower.includes(m)). No I/O, no state, idempotent.
|
|
89
|
+
* @throws TypeError if `text` is not a string.
|
|
90
|
+
*/
|
|
91
|
+
export function hasContextPressureMarker(text) {
|
|
92
|
+
if (typeof text !== "string") {
|
|
93
|
+
throw new TypeError(TEXT_NOT_A_STRING);
|
|
94
|
+
}
|
|
95
|
+
const lower = text.toLowerCase();
|
|
96
|
+
return CONTEXT_PRESSURE_MARKERS.some((marker) => lower.includes(marker));
|
|
97
|
+
}
|
|
98
|
+
/** Alias used by the prompt-level guard (guard 1). Identical semantics to hasContextPressureMarker. */
|
|
99
|
+
export function isContextPressurePrompt(prompt) {
|
|
100
|
+
return hasContextPressureMarker(prompt);
|
|
101
|
+
}
|
|
102
|
+
// --- Guard 2: idempotency (already-injected) ---------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* Generic per-mode idempotency check (RC1). Reads the TAIL (last TRANSCRIPT_SEARCH_BYTES bytes) of
|
|
105
|
+
* the transcript, parses each JSONL line, and returns true iff some line is a prior HOOK OUTPUT
|
|
106
|
+
* envelope: parsed.hookSpecificOutput.hookEventName === HOOK_OUTPUT_EVENT_NAME AND
|
|
107
|
+
* typeof additionalContext === "string" AND additionalContext.includes(`marker`). A marker quoted
|
|
108
|
+
* inside a user/assistant content STRING (or a forged envelope embedded as text) is NOT a top-level
|
|
109
|
+
* envelope line and is skipped. FAIL-OPEN: any thrown Error => false. The multi-mode hook passes the
|
|
110
|
+
* MATCHED mode's open marker so re-injection is suppressed per mode (mode switching is allowed).
|
|
111
|
+
*/
|
|
112
|
+
export function transcriptHasDirectiveMarker(transcriptPath, marker) {
|
|
113
|
+
if (transcriptPath === null || transcriptPath === undefined) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const buf = readFileSync(transcriptPath);
|
|
118
|
+
const tail = buf.subarray(Math.max(0, buf.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
|
|
119
|
+
for (const line of tail.split(/\r?\n/)) {
|
|
120
|
+
const parsed = parseJsonLine(line);
|
|
121
|
+
if (parsed === null || !isRecord(parsed)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const hso = parsed["hookSpecificOutput"];
|
|
125
|
+
if (!isRecord(hso)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (hso["hookEventName"] !== HOOK_OUTPUT_EVENT_NAME) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const additionalContext = hso["additionalContext"];
|
|
132
|
+
if (typeof additionalContext === "string" && additionalContext.includes(marker)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof Error) {
|
|
140
|
+
return false; // fail-open
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Lit-loop idempotency guard (guard 2) — the generic check bound to the lit-loop marker. */
|
|
146
|
+
export function transcriptHasLitLoopDirective(transcriptPath) {
|
|
147
|
+
return transcriptHasDirectiveMarker(transcriptPath, LIT_LOOP_DIRECTIVE_MARKER);
|
|
148
|
+
}
|
|
149
|
+
// --- Guard 3: context-pressure transcript ------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* Context-pressure transcript guard (guard 3). Reads a bounded head+tail window of the transcript
|
|
152
|
+
* (A3 addendum §A): the whole file when ≤ 2×GUARD3_WINDOW_BYTES, else the first + last
|
|
153
|
+
* GUARD3_WINDOW_BYTES joined by a "\n" separator (so a marker cannot be manufactured across the
|
|
154
|
+
* cut), then runs hasContextPressureMarker over it. Markers buried in nested JSON are caught by
|
|
155
|
+
* design. FAIL-OPEN: any thrown Error => false. null/undefined path => false.
|
|
156
|
+
*/
|
|
157
|
+
export function transcriptHasContextPressureMarker(transcriptPath) {
|
|
158
|
+
if (transcriptPath === null || transcriptPath === undefined) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const buf = readFileSync(transcriptPath);
|
|
163
|
+
const n = buf.byteLength;
|
|
164
|
+
let text;
|
|
165
|
+
if (n <= 2 * GUARD3_WINDOW_BYTES) {
|
|
166
|
+
text = buf.toString("utf8");
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const head = buf.subarray(0, GUARD3_WINDOW_BYTES).toString("utf8");
|
|
170
|
+
const tail = buf.subarray(n - GUARD3_WINDOW_BYTES).toString("utf8");
|
|
171
|
+
text = `${head}\n${tail}`;
|
|
172
|
+
}
|
|
173
|
+
return hasContextPressureMarker(text);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error instanceof Error) {
|
|
177
|
+
return false; // fail-open
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// --- Single entry point (A3 C3) -----------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* THE single decision the M06 hook runner calls. Evaluates the four guards in fixed order and
|
|
185
|
+
* returns the first veto, else {suppress:false, reason:"none"}. Suppression dominates activation:
|
|
186
|
+
* guard 0 (not-a-trigger) runs first so the file-I/O guards only fire when a trigger is present;
|
|
187
|
+
* guard 1 (context-pressure-prompt) runs before the transcript guards so a recovery prompt that
|
|
188
|
+
* also contains `lit` is suppressed without any file read. Never throws for any string prompt +
|
|
189
|
+
* GuardTranscriptInput path (the only throw is the non-string prompt defensive guard).
|
|
190
|
+
* @throws TypeError only if `prompt` is not a string (M06 type-guards this upstream).
|
|
191
|
+
*/
|
|
192
|
+
export function shouldSuppressInjection(prompt, transcriptPath, openMarker) {
|
|
193
|
+
if (typeof prompt !== "string") {
|
|
194
|
+
throw new TypeError(PROMPT_NOT_A_STRING);
|
|
195
|
+
}
|
|
196
|
+
if (!isLitTriggerPrompt(prompt)) {
|
|
197
|
+
return { suppress: true, reason: "not-a-trigger" };
|
|
198
|
+
}
|
|
199
|
+
if (isContextPressurePrompt(prompt)) {
|
|
200
|
+
return { suppress: true, reason: "context-pressure-prompt" };
|
|
201
|
+
}
|
|
202
|
+
// RC1: guard 2 checks the MATCHED mode's marker only — re-injecting the SAME mode is suppressed,
|
|
203
|
+
// switching modes (e.g. `lit` after `litwork`) is allowed.
|
|
204
|
+
if (transcriptHasDirectiveMarker(transcriptPath, openMarker)) {
|
|
205
|
+
return { suppress: true, reason: "already-injected" };
|
|
206
|
+
}
|
|
207
|
+
if (transcriptHasContextPressureMarker(transcriptPath)) {
|
|
208
|
+
return { suppress: true, reason: "context-pressure-transcript" };
|
|
209
|
+
}
|
|
210
|
+
return { suppress: false, reason: "none" };
|
|
211
|
+
}
|
|
212
|
+
/** Lit-loop entry point (backward-compatible) — the generic decision bound to the lit-loop marker. */
|
|
213
|
+
export function shouldSuppressLitLoopInjection(prompt, transcriptPath) {
|
|
214
|
+
return shouldSuppressInjection(prompt, transcriptPath, LIT_LOOP_DIRECTIVE_MARKER);
|
|
215
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Hard cap on stdin to bound memory / ReDoS exposure inside the 5 s Codex hook budget. 8 MB. */
|
|
2
|
+
export declare const MAX_STDIN_BYTES = 8000000;
|
|
3
|
+
/** Machine-readable error envelope emitted to stderr on a NON-zero exit. */
|
|
4
|
+
export interface LitHookError {
|
|
5
|
+
readonly ok: false;
|
|
6
|
+
readonly error: {
|
|
7
|
+
readonly code: LitHookErrorCode;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export type LitHookErrorCode = "LIT_HOOK_STDIN_INVALID_JSON" | "LIT_HOOK_STDIN_TOO_LARGE";
|
|
12
|
+
/**
|
|
13
|
+
* Stream-driven entry point. Resolves exactly one exit code (0 or 2); NEVER calls process.exit and
|
|
14
|
+
* NEVER throws. Writes the camelCase activation line to stdout (empty on no-op), or a LitHookError
|
|
15
|
+
* line to stderr on malformed / oversized stdin.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runUserPromptSubmitHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream): Promise<number>;
|