litcodex-ai 0.3.7 → 0.3.9
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/dist/config-migration/toml-shape.js +36 -2
- package/node_modules/@litcodex/lit-loop/directives/lit-plan.md +28 -13
- package/node_modules/@litcodex/lit-loop/dist/codex-goal-instruction.js +49 -17
- package/node_modules/@litcodex/lit-loop/dist/goal-status.d.ts +4 -2
- package/node_modules/@litcodex/lit-loop/dist/goal-status.js +10 -3
- package/node_modules/@litcodex/lit-loop/dist/loop-cli.js +5 -2
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor.d.ts +13 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-doctor.js +54 -1
- package/node_modules/@litcodex/lit-loop/dist/loop-errors.d.ts +1 -1
- package/node_modules/@litcodex/lit-loop/dist/loop-errors.js +1 -0
- package/node_modules/@litcodex/lit-loop/dist/loop-handlers.js +22 -1
- package/node_modules/@litcodex/lit-loop/dist/state-paths.d.ts +5 -3
- package/node_modules/@litcodex/lit-loop/dist/state-paths.js +15 -5
- package/node_modules/@litcodex/lit-loop/dist/state-store.js +8 -0
- package/node_modules/@litcodex/lit-loop/dist/state-types.d.ts +3 -0
- package/node_modules/@litcodex/lit-loop/package.json +1 -1
- package/package.json +2 -2
|
@@ -28,8 +28,10 @@ export function validateTomlShape(config) {
|
|
|
28
28
|
const trimmed = raw.trim();
|
|
29
29
|
const isHeader = trimmed.startsWith("[");
|
|
30
30
|
if (isHeader) {
|
|
31
|
-
// R2 — unbalanced [section] header.
|
|
32
|
-
|
|
31
|
+
// R2 — unbalanced [section] header. Strip quoted key segments FIRST so brackets inside a
|
|
32
|
+
// quoted key (e.g. a Windows project path `[projects."C:\…\[INBOX] MEMOS"]`, which Codex
|
|
33
|
+
// writes verbatim and is valid TOML) are not mistaken for stray section brackets.
|
|
34
|
+
if (!WELL_FORMED_HEADER.test(stripQuotedSpans(raw))) {
|
|
33
35
|
return reject("unbalanced-section-header", lineNo);
|
|
34
36
|
}
|
|
35
37
|
// R3 — duplicate [features.multi_agent_v2] table.
|
|
@@ -105,3 +107,35 @@ function stripInlineComment(line) {
|
|
|
105
107
|
const idx = line.indexOf("#");
|
|
106
108
|
return idx === -1 ? line.trim() : line.slice(0, idx).trim();
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Replace every quoted span (basic `"…"` or literal `'…'`) with a single inert placeholder, so a
|
|
112
|
+
* structural scan never sees `[`, `]`, `#`, or quote chars that live INSIDE a TOML string. Basic
|
|
113
|
+
* strings honor `\` escapes (so an escaped quote doesn't close early); literal strings do not.
|
|
114
|
+
* Used to bracket-balance a section header whose key may contain a quoted path (e.g. `[INBOX]`).
|
|
115
|
+
*/
|
|
116
|
+
function stripQuotedSpans(line) {
|
|
117
|
+
let out = "";
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < line.length) {
|
|
120
|
+
const ch = line[i];
|
|
121
|
+
if (ch === '"' || ch === "'") {
|
|
122
|
+
out += "Q"; // the whole quoted span collapses to one bracket-free key token
|
|
123
|
+
i += 1;
|
|
124
|
+
while (i < line.length) {
|
|
125
|
+
if (ch === '"' && line[i] === "\\") {
|
|
126
|
+
i += 2; // skip an escaped char in a basic string (e.g. \" or \\)
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (line[i] === ch) {
|
|
130
|
+
i += 1; // closing quote
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
i += 1;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
out += ch;
|
|
138
|
+
i += 1;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
@@ -21,6 +21,17 @@ A plan is **decision-complete** when the implementer needs ZERO judgment calls:
|
|
|
21
21
|
every decision made, every ambiguity resolved, every pattern referenced with a
|
|
22
22
|
concrete path.
|
|
23
23
|
|
|
24
|
+
# Native Codex Plan Mode cooperation
|
|
25
|
+
|
|
26
|
+
Native Codex Plan Mode is a host UI state, not a LitCodex hook setting. LitCodex
|
|
27
|
+
cannot switch Codex into native Plan Mode from `UserPromptSubmit`; if the user
|
|
28
|
+
wants host-enforced native planning, tell them to press Shift+Tab and resend or
|
|
29
|
+
continue. If native Codex Plan Mode is already active, keep the turn read-only,
|
|
30
|
+
do not write `.litcodex/drafts` or `.litcodex/plans`, and return the final plan
|
|
31
|
+
as a complete `<proposed_plan>...</proposed_plan>` block. If native mode is not
|
|
32
|
+
active and the user only invoked lit-plan, proceed with the file-backed lit-plan
|
|
33
|
+
workflow below.
|
|
34
|
+
|
|
24
35
|
# The gate (non-negotiable behavior)
|
|
25
36
|
- **Explore before asking.** Most "questions" are discoverable facts. Ground
|
|
26
37
|
yourself in the repo with read-only tools and parallel research subagents
|
|
@@ -101,11 +112,13 @@ paths as `dirty_worktree` risk, keep them out of task scope, and require
|
|
|
101
112
|
verifiers to reject plans that would overwrite user changes.
|
|
102
113
|
|
|
103
114
|
# Phase 2 — Interview (ask only what exploration cannot resolve)
|
|
104
|
-
|
|
115
|
+
When not in native Codex Plan Mode, record everything to `.litcodex/drafts/<slug>.md` as you go: confirmed
|
|
105
116
|
requirements (the user's exact words), decisions + rationale, research findings,
|
|
106
117
|
open questions, scope IN / OUT. Update it after EVERY meaningful exchange — long
|
|
107
118
|
interviews outlive your context, and plan generation reads the draft, not your
|
|
108
|
-
memory.
|
|
119
|
+
memory. In native Codex Plan Mode, keep equivalent notes in the response only;
|
|
120
|
+
do not create files until the user leaves native mode or explicitly grants a
|
|
121
|
+
write-capable planning pass.
|
|
109
122
|
|
|
110
123
|
Interview focus, informed by Phase 1 findings: goal + definition of done, scope
|
|
111
124
|
boundaries (IN and explicitly OUT), technical approach ("I found pattern X at
|
|
@@ -150,11 +163,13 @@ automatic interview-to-plan transition.
|
|
|
150
163
|
assumptions, missing acceptance criteria. SCOPE: this planning session.
|
|
151
164
|
VERIFY: each gap names a concrete fix.","fork_context":false})`. Fold the
|
|
152
165
|
findings in silently.
|
|
153
|
-
2.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
2. If native Codex Plan Mode is active, return ONE complete
|
|
167
|
+
`<proposed_plan>...</proposed_plan>` using the template below and do not write
|
|
168
|
+
a file. Otherwise write ONE plan to `.litcodex/plans/<slug>.md`. No
|
|
169
|
+
"Phase 1 plan / Phase 2 plan" splits; 50+ todos is fine. Build it
|
|
170
|
+
incrementally — skeleton first, then append todo batches — so output limits
|
|
171
|
+
never truncate it; re-read the file with the `shell` tool to confirm
|
|
172
|
+
completeness.
|
|
158
173
|
3. **Self-review:** every todo has references + agent-executable acceptance
|
|
159
174
|
criteria + QA scenarios; no business-logic assumption without evidence; zero
|
|
160
175
|
acceptance criteria require a human.
|
|
@@ -243,12 +258,12 @@ Every `multi_agent_v1.spawn_agent` message is self-contained and starts with
|
|
|
243
258
|
context the child needs. Name any skills the child needs directly inside its
|
|
244
259
|
`message`.
|
|
245
260
|
|
|
246
|
-
|
|
247
|
-
`
|
|
248
|
-
`
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
Prefer exact installed LitCodex role names when the host accepts `agent_type`:
|
|
262
|
+
`litcodex-explorer`, `litcodex-librarian`, `litcodex-plan`, `litcodex-metis`,
|
|
263
|
+
`litcodex-momus`, and `litcodex-litwork-reviewer`. If the tool exposes no
|
|
264
|
+
`agent_type` parameter or rejects the role, omit `agent_type` and paste the role
|
|
265
|
+
requirements into `message`. Judge the result from delivered evidence, not from
|
|
266
|
+
the route selected.
|
|
252
267
|
|
|
253
268
|
Plan and reviewer agents may run for a long time; spawn them in the background,
|
|
254
269
|
keep doing independent root work, and poll with short `multi_agent_v1.wait_agent`
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Builds the `create_goal` / `update_goal` instruction text and JSON payload that lit-loop's
|
|
4
4
|
// handleRun and handleCheckpoint append to their output so the agent keeps Codex's `/goal`
|
|
5
|
-
// surface in sync with the
|
|
6
|
-
|
|
7
|
-
import { expectedCodexObjective, isFinalRunCompletionCandidate, isLitLoopDone } from "./goal-status.js";
|
|
5
|
+
// surface in sync with the durable lit-loop plan. Pure — no I/O, no store, no clock.
|
|
6
|
+
import { codexGoalMode, expectedCodexObjective, isFinalRunCompletionCandidate, isLitLoopDone } from "./goal-status.js";
|
|
8
7
|
export function buildCodexGoalInstruction(args) {
|
|
9
8
|
const { plan, goal } = args;
|
|
10
|
-
const json = { objective: expectedCodexObjective(goal) };
|
|
9
|
+
const json = { objective: expectedCodexObjective(plan, goal) };
|
|
11
10
|
const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(plan, goal);
|
|
12
11
|
const text = buildText(plan, goal, json, isFinal);
|
|
13
12
|
return { text, json };
|
|
14
13
|
}
|
|
15
14
|
function buildText(plan, goal, payload, isFinal) {
|
|
15
|
+
const mode = codexGoalMode(plan);
|
|
16
16
|
return joinLines([
|
|
17
|
-
"lit-loop Codex goal handoff",
|
|
17
|
+
mode === "aggregate" ? "lit-loop aggregate Codex goal handoff" : "lit-loop Codex goal handoff",
|
|
18
18
|
`Plan: ${plan.goalsPath}`,
|
|
19
19
|
`Ledger: ${plan.ledgerPath}`,
|
|
20
20
|
`Goal: ${goal.id} — ${goal.title}`,
|
|
@@ -26,17 +26,31 @@ function buildText(plan, goal, payload, isFinal) {
|
|
|
26
26
|
"Codex goal integration constraints:",
|
|
27
27
|
"- Use the create_goal payload exactly as rendered: objective only.",
|
|
28
28
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"- If a different active Codex goal exists, finish or clear it (`/goal clear`) before starting this lit-loop goal.",
|
|
32
|
-
"- Work only this goal until all criteria pass.",
|
|
33
|
-
...finalLines(goal, isFinal),
|
|
29
|
+
...nativeGoalModeLines(mode),
|
|
30
|
+
...finalLines(goal, isFinal, mode),
|
|
34
31
|
...checkpointLines(),
|
|
35
32
|
"",
|
|
36
33
|
"create_goal payload:",
|
|
37
34
|
JSON.stringify(payload, null, 2),
|
|
38
35
|
]);
|
|
39
36
|
}
|
|
37
|
+
function nativeGoalModeLines(mode) {
|
|
38
|
+
if (mode === "aggregate") {
|
|
39
|
+
return [
|
|
40
|
+
"- This Codex goal represents the whole lit-loop plan, not just the active story.",
|
|
41
|
+
"- First call get_goal. If no active goal exists, call create_goal with the payload below.",
|
|
42
|
+
"- If get_goal already reports this aggregate objective as active, continue without creating a new goal.",
|
|
43
|
+
"- If a different active Codex goal exists, finish or clear it (`/goal clear`) before starting this lit-loop plan.",
|
|
44
|
+
"- Work only the active lit-loop goal until its criteria pass; the aggregate Codex goal remains active across later lit-loop goals.",
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
return [
|
|
48
|
+
"- First call get_goal. If no active goal exists, call create_goal with the payload below.",
|
|
49
|
+
"- If get_goal already reports this objective as active, continue without creating a new goal.",
|
|
50
|
+
"- If a different active Codex goal exists, finish or clear it (`/goal clear`) before starting this lit-loop goal.",
|
|
51
|
+
"- Work only this goal until all criteria pass.",
|
|
52
|
+
];
|
|
53
|
+
}
|
|
40
54
|
function activeGoalLines(goal) {
|
|
41
55
|
return ["Active goal:", `- id: ${goal.id}`, `- title: ${goal.title}`, `- objective: ${goal.objective}`];
|
|
42
56
|
}
|
|
@@ -51,9 +65,14 @@ function successCriteriaLines(goal) {
|
|
|
51
65
|
}),
|
|
52
66
|
];
|
|
53
67
|
}
|
|
54
|
-
function finalLines(goal, isFinal) {
|
|
68
|
+
function finalLines(goal, isFinal, mode) {
|
|
55
69
|
if (!isFinal) {
|
|
56
|
-
return
|
|
70
|
+
return mode === "aggregate"
|
|
71
|
+
? [
|
|
72
|
+
"- This is not the final lit-loop goal; leave the aggregate Codex goal active for the next run.",
|
|
73
|
+
"- Do not call update_goal yet; complete only the current lit-loop goal in durable state.",
|
|
74
|
+
]
|
|
75
|
+
: ["- This is not the final lit-loop goal; leave the Codex goal active for the next run."];
|
|
57
76
|
}
|
|
58
77
|
return [
|
|
59
78
|
"- This is the final lit-loop goal. After all criteria pass and checkpoint is complete:",
|
|
@@ -70,18 +89,31 @@ function checkpointLines() {
|
|
|
70
89
|
// ── checkpoint handoff ──────────────────────────────────────────────────────
|
|
71
90
|
export function buildCodexGoalCheckpoint(args) {
|
|
72
91
|
const { plan, goal, status } = args;
|
|
92
|
+
const mode = codexGoalMode(plan);
|
|
73
93
|
if (status === "complete") {
|
|
74
|
-
const lines = [
|
|
75
|
-
"Codex goal checkpoint:",
|
|
76
|
-
`Goal ${goal.id} is complete. Call update_goal({status: "complete"}) to mark the Codex goal done.`,
|
|
77
|
-
];
|
|
78
94
|
if (isLitLoopDone(plan)) {
|
|
95
|
+
const lines = [
|
|
96
|
+
"Codex goal checkpoint:",
|
|
97
|
+
`Goal ${goal.id} is complete. Call update_goal({status: "complete"}) to mark the Codex goal done.`,
|
|
98
|
+
];
|
|
79
99
|
lines.push("All lit-loop goals are now complete. Run `/goal clear` to close the Codex goal surface.");
|
|
100
|
+
return joinLines(lines);
|
|
101
|
+
}
|
|
102
|
+
if (mode === "aggregate") {
|
|
103
|
+
return joinLines([
|
|
104
|
+
"Codex goal checkpoint:",
|
|
105
|
+
`Goal ${goal.id} is complete. The aggregate Codex goal remains active for the rest of the durable plan.`,
|
|
106
|
+
"Do not call update_goal yet; run `litcodex loop run` to hand off the next goal.",
|
|
107
|
+
]);
|
|
80
108
|
}
|
|
81
109
|
else {
|
|
110
|
+
const lines = [
|
|
111
|
+
"Codex goal checkpoint:",
|
|
112
|
+
`Goal ${goal.id} is complete. Call update_goal({status: "complete"}) to mark the Codex goal done.`,
|
|
113
|
+
];
|
|
82
114
|
lines.push("Run `litcodex loop run` to hand off the next goal.");
|
|
115
|
+
return joinLines(lines);
|
|
83
116
|
}
|
|
84
|
-
return joinLines(lines);
|
|
85
117
|
}
|
|
86
118
|
return joinLines([
|
|
87
119
|
"Codex goal checkpoint:",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { LoopGoal, LoopPlan } from "./loop-types.js";
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
2
|
+
export declare function codexGoalMode(plan: LoopPlan): "aggregate" | "per_story";
|
|
3
|
+
export declare function aggregateCodexObjective(plan: LoopPlan): string;
|
|
4
|
+
/** The Codex goal objective for a lit-loop plan/goal. Defaults to one aggregate goal per plan. */
|
|
5
|
+
export declare function expectedCodexObjective(plan: LoopPlan, goal: LoopGoal): string;
|
|
4
6
|
/** True when the goal has at least one criterion and every criterion is `pass`. */
|
|
5
7
|
export declare function hasAllCriteriaPass(goal: LoopGoal): boolean;
|
|
6
8
|
/** True when every goal in the plan is `complete`. */
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Predicates and objective resolver for the lit-loop ↔ Codex `/goal` handoff. All functions are
|
|
4
4
|
// PURE — no I/O, no store, no clock. Imports only loop-types (which re-exports state-types).
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export function codexGoalMode(plan) {
|
|
6
|
+
return plan.codexGoalMode ?? "aggregate";
|
|
7
|
+
}
|
|
8
|
+
export function aggregateCodexObjective(plan) {
|
|
9
|
+
return (plan.codexObjective ??
|
|
10
|
+
`Complete the durable lit-loop plan in ${plan.goalsPath}, including any later accepted/appended goals; use ${plan.ledgerPath} as the audit trail.`);
|
|
11
|
+
}
|
|
12
|
+
/** The Codex goal objective for a lit-loop plan/goal. Defaults to one aggregate goal per plan. */
|
|
13
|
+
export function expectedCodexObjective(plan, goal) {
|
|
14
|
+
return codexGoalMode(plan) === "per_story" ? goal.objective : aggregateCodexObjective(plan);
|
|
8
15
|
}
|
|
9
16
|
/** True when the goal has at least one criterion and every criterion is `pass`. */
|
|
10
17
|
export function hasAllCriteriaPass(goal) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// store PLAN_MISSING→3, PLAN_CORRUPT→4, WRITE_FAILED→5; unknown subcommand/usage→1; bad args→2;
|
|
10
10
|
// not-found→3. The `doctor` route delegates to the canonical M11 6-check doctor (A3 C5) and ALWAYS
|
|
11
11
|
// resolves exit 0 (a diagnostic, never a gate).
|
|
12
|
-
import { renderDoctorJson, renderDoctorText, runLoopDoctor } from "./loop-doctor.js";
|
|
12
|
+
import { hookRegisteredInstalled, renderDoctorJson, renderDoctorText, runLoopDoctor } from "./loop-doctor.js";
|
|
13
13
|
import { exitCodeForLoop, LitLoopError } from "./loop-errors.js";
|
|
14
14
|
import { handleCheckpoint, handleCreate, handleRecordEvidence, handleRun, handleStatus, hasFlag, } from "./loop-handlers.js";
|
|
15
15
|
import { resolveLoopScope } from "./state-store.js";
|
|
@@ -59,7 +59,10 @@ export async function loopCommand(argv, io, clock) {
|
|
|
59
59
|
const scope = resolveLoopScope({ argv: rest, env: process.env });
|
|
60
60
|
if (head === "doctor") {
|
|
61
61
|
// A3 C5: delegate to the canonical M11 6-check doctor; ALWAYS exit 0 (diagnostic, not gate).
|
|
62
|
-
|
|
62
|
+
// Use the INSTALL-AWARE hook probe: a real `litcodex loop doctor` runs from an arbitrary cwd
|
|
63
|
+
// (no dev-tree manifest), so the pure repoRoot-only probe falsely warns "hook not registered".
|
|
64
|
+
// hookRegisteredInstalled also checks where the installed plugin actually lives (Codex cache).
|
|
65
|
+
const report = await runLoopDoctor({ repoRoot, scope }, { hookRegistered: (r) => hookRegisteredInstalled(r) });
|
|
63
66
|
stdout.write(json ? renderDoctorJson(report) : renderDoctorText(report));
|
|
64
67
|
return 0;
|
|
65
68
|
}
|
|
@@ -12,6 +12,19 @@ export declare const TERMINAL_LEDGER_KINDS: {
|
|
|
12
12
|
* catch (→ warn). Never executes any command from the manifest.
|
|
13
13
|
*/
|
|
14
14
|
export declare function hookRegistered(repoRoot: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Install-aware hook probe (production default the global CLI injects). The dev-tree manifest
|
|
17
|
+
* `<repoRoot>/plugins/litcodex/hooks/hooks.json` ONLY exists when run from the repo; a real install
|
|
18
|
+
* (`litcodex loop doctor` from any cwd) has no such file, so the pure `hookRegistered` falsely warns
|
|
19
|
+
* "not registered". This trusts the repoRoot manifest when present (dev/test), and otherwise looks
|
|
20
|
+
* where the INSTALLED plugin actually lives: the Codex plugin cache
|
|
21
|
+
* `<CODEX_HOME>/plugins/cache/.../hooks/hooks.json` (the global CLI runs the bundled lit-loop, so the
|
|
22
|
+
* plugin's aggregate manifest only exists in that cache). Pure-read, never executes a command;
|
|
23
|
+
* `opts.codexHome` is injectable so tests stay hermetic.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hookRegisteredInstalled(repoRoot: string, opts?: {
|
|
26
|
+
codexHome?: string;
|
|
27
|
+
}): Promise<boolean>;
|
|
15
28
|
/** Scan the ledger tail backward for the last terminal entry; null when none usable. */
|
|
16
29
|
export declare function latestCheckpointFromLedger(entries: ReadonlyArray<Record<string, unknown>>): LoopCheckpointRef | null;
|
|
17
30
|
/**
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Imports flat siblings `./state-store.js`/`./state-paths.js`/`./loop-model.js`/`./loop-types.js`
|
|
13
13
|
// (A3 C9 — NEVER `../state/...`). No `node:fs` import: existence-probing is M08 `statExists` only.
|
|
14
|
-
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
import { sanitizeId } from "./loop-doctor-render.js";
|
|
17
18
|
import { summarizePlan } from "./loop-model.js";
|
|
@@ -52,6 +53,58 @@ export async function hookRegistered(repoRoot) {
|
|
|
52
53
|
const parsed = JSON.parse(raw);
|
|
53
54
|
return scanForCommand(parsed);
|
|
54
55
|
}
|
|
56
|
+
/** Read + scan a manifest at an absolute path; ENOENT/parse-error ⇒ false (never throws). */
|
|
57
|
+
async function scanManifestAt(manifestAbs) {
|
|
58
|
+
try {
|
|
59
|
+
if (!(await statExists(manifestAbs)))
|
|
60
|
+
return false;
|
|
61
|
+
return scanForCommand(JSON.parse(await readFile(manifestAbs, "utf8")));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Walk `<cacheRoot>/<marketplace>/<plugin>/<version>/hooks/hooks.json` (bounded; a few installs). */
|
|
68
|
+
async function scanCodexPluginCache(cacheRoot) {
|
|
69
|
+
const listDirs = async (dir) => {
|
|
70
|
+
try {
|
|
71
|
+
return await readdir(dir);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
for (const mp of await listDirs(cacheRoot)) {
|
|
78
|
+
for (const plugin of await listDirs(join(cacheRoot, mp))) {
|
|
79
|
+
for (const ver of await listDirs(join(cacheRoot, mp, plugin))) {
|
|
80
|
+
if (await scanManifestAt(join(cacheRoot, mp, plugin, ver, "hooks", "hooks.json")))
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Install-aware hook probe (production default the global CLI injects). The dev-tree manifest
|
|
89
|
+
* `<repoRoot>/plugins/litcodex/hooks/hooks.json` ONLY exists when run from the repo; a real install
|
|
90
|
+
* (`litcodex loop doctor` from any cwd) has no such file, so the pure `hookRegistered` falsely warns
|
|
91
|
+
* "not registered". This trusts the repoRoot manifest when present (dev/test), and otherwise looks
|
|
92
|
+
* where the INSTALLED plugin actually lives: the Codex plugin cache
|
|
93
|
+
* `<CODEX_HOME>/plugins/cache/.../hooks/hooks.json` (the global CLI runs the bundled lit-loop, so the
|
|
94
|
+
* plugin's aggregate manifest only exists in that cache). Pure-read, never executes a command;
|
|
95
|
+
* `opts.codexHome` is injectable so tests stay hermetic.
|
|
96
|
+
*/
|
|
97
|
+
export async function hookRegisteredInstalled(repoRoot, opts) {
|
|
98
|
+
// 1. Dev tree — authoritative when present (preserves dev/test behavior and keeps the real-fs cache
|
|
99
|
+
// walk below from ever running inside the repo's own hermetic unit tests).
|
|
100
|
+
const devManifest = join(repoRoot, ...HOOK_MANIFEST_RELPATH.split("/"));
|
|
101
|
+
if (await statExists(devManifest)) {
|
|
102
|
+
return scanManifestAt(devManifest);
|
|
103
|
+
}
|
|
104
|
+
// 2. Codex plugin cache — the real install context for a global-CLI `litcodex loop doctor`.
|
|
105
|
+
const codexHome = opts?.codexHome ?? (process.env["CODEX_HOME"]?.trim() || join(homedir(), ".codex"));
|
|
106
|
+
return scanCodexPluginCache(join(codexHome, "plugins", "cache"));
|
|
107
|
+
}
|
|
55
108
|
/** Recursively scan every string value under a `command` key for ALL three fragments. */
|
|
56
109
|
function scanForCommand(node) {
|
|
57
110
|
if (Array.isArray(node)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Stable SCREAMING_SNAKE error codes; consumers map them to an exit code via `.code`. */
|
|
2
|
-
export type LoopErrorCode = "LIT_LOOP_SUBCOMMAND_UNKNOWN" | "LIT_LOOP_ARGUMENT_MISSING" | "LIT_LOOP_ARGUMENT_INVALID" | "LIT_LOOP_BRIEF_REQUIRED" | "LIT_LOOP_BRIEF_FILE_UNREADABLE" | "LIT_LOOP_EVIDENCE_STATUS_INVALID" | "LIT_LOOP_GOAL_NOT_FOUND" | "LIT_LOOP_CRITERION_NOT_FOUND" | "LIT_LOOP_CRITERIA_NOT_ALL_PASS";
|
|
2
|
+
export type LoopErrorCode = "LIT_LOOP_SUBCOMMAND_UNKNOWN" | "LIT_LOOP_ARGUMENT_MISSING" | "LIT_LOOP_ARGUMENT_INVALID" | "LIT_LOOP_BRIEF_REQUIRED" | "LIT_LOOP_BRIEF_FILE_UNREADABLE" | "LIT_LOOP_PLAN_EXISTS_COMPLETE" | "LIT_LOOP_EVIDENCE_STATUS_INVALID" | "LIT_LOOP_GOAL_NOT_FOUND" | "LIT_LOOP_CRITERION_NOT_FOUND" | "LIT_LOOP_CRITERIA_NOT_ALL_PASS";
|
|
3
3
|
/** Machine-readable CLI error. `code` is the stable token; the exit code is derived from it. */
|
|
4
4
|
export declare class LitLoopError extends Error {
|
|
5
5
|
readonly name = "LitLoopError";
|
|
@@ -36,6 +36,7 @@ export function exitCodeForLoop(err) {
|
|
|
36
36
|
case "LIT_LOOP_GOAL_NOT_FOUND":
|
|
37
37
|
case "LIT_LOOP_CRITERION_NOT_FOUND":
|
|
38
38
|
case "LIT_LOOP_CRITERIA_NOT_ALL_PASS":
|
|
39
|
+
case "LIT_LOOP_PLAN_EXISTS_COMPLETE":
|
|
39
40
|
return 3; // not found / unresolved
|
|
40
41
|
default:
|
|
41
42
|
return exitCodeFor(err); // store codes (3/4/5) + unexpected → 1
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// `./state-store.js` (A3 C9) — this module never touches node:fs. Errors are the CLI's
|
|
7
7
|
// `LitLoopError` (mapped on `.code` by loop-cli's exitCodeForLoop) or re-thrown store errors.
|
|
8
8
|
import { buildCodexGoalCheckpoint, buildCodexGoalInstruction } from "./codex-goal-instruction.js";
|
|
9
|
+
import { aggregateCodexObjective, codexGoalMode, isLitLoopDone } from "./goal-status.js";
|
|
9
10
|
import { LitLoopError } from "./loop-errors.js";
|
|
10
11
|
import { buildRunInstruction, deriveGoalCandidates, normalizeGoalId, pickNextRunnableGoal, requireAllCriteriaPass, seedDefaultSuccessCriteria, summarizePlan, titleFromObjective, } from "./loop-model.js";
|
|
11
12
|
import { LOOP_CREATE_STDOUT } from "./loop-stdout.js";
|
|
@@ -71,6 +72,9 @@ export async function handleCreate(argv, ctx) {
|
|
|
71
72
|
return withMutationLock(ctx.repoRoot, ctx.scope, async () => {
|
|
72
73
|
const existing = await readPlanOrNull(ctx);
|
|
73
74
|
if (existing !== null && !force) {
|
|
75
|
+
if (isLitLoopDone(existing)) {
|
|
76
|
+
throw new LitLoopError(`Existing lit-loop plan is already complete at ${existing.goalsPath}. Start fresh with litcodex loop create --session <new-id> or use --force to overwrite intentionally.`, "LIT_LOOP_PLAN_EXISTS_COMPLETE", { goalsPath: existing.goalsPath });
|
|
77
|
+
}
|
|
74
78
|
return planResult(existing); // idempotent no-op
|
|
75
79
|
}
|
|
76
80
|
const now = ctx.now();
|
|
@@ -92,8 +96,10 @@ export async function handleCreate(argv, ctx) {
|
|
|
92
96
|
updatedAt: now,
|
|
93
97
|
...paths,
|
|
94
98
|
sessionId: ctx.scope?.sessionId ?? null,
|
|
99
|
+
codexGoalMode: "aggregate",
|
|
95
100
|
goals,
|
|
96
101
|
};
|
|
102
|
+
plan.codexObjective = aggregateCodexObjective(plan);
|
|
97
103
|
await writeBrief(ctx.repoRoot, brief, ctx.scope);
|
|
98
104
|
await writePlan(ctx.repoRoot, plan, ctx.scope);
|
|
99
105
|
await appendLedger(ctx.repoRoot, { at: now, kind: "plan_created", message: `${goals.length} goal(s) derived from brief.` }, ctx.scope);
|
|
@@ -211,13 +217,28 @@ export async function handleCheckpoint(argv, ctx) {
|
|
|
211
217
|
await writePlan(ctx.repoRoot, plan, ctx.scope);
|
|
212
218
|
await appendLedger(ctx.repoRoot, ledgerEntry, ctx.scope);
|
|
213
219
|
const codexCheckpoint = buildCodexGoalCheckpoint({ plan, goal, status });
|
|
220
|
+
const codexGoalStatus = checkpointCodexGoalStatus(plan, status);
|
|
214
221
|
return {
|
|
215
222
|
exitCode: 0,
|
|
216
223
|
text: `lit-loop checkpoint: ${goalId} -> ${status}\n\n${codexCheckpoint}\n`,
|
|
217
|
-
json: {
|
|
224
|
+
json: {
|
|
225
|
+
ok: true,
|
|
226
|
+
goal,
|
|
227
|
+
ledgerEntry,
|
|
228
|
+
codexGoal: { status: codexGoalStatus },
|
|
229
|
+
plan,
|
|
230
|
+
summary: summarizePlan(plan),
|
|
231
|
+
},
|
|
218
232
|
};
|
|
219
233
|
});
|
|
220
234
|
}
|
|
235
|
+
function checkpointCodexGoalStatus(plan, status) {
|
|
236
|
+
if (status !== "complete")
|
|
237
|
+
return "active";
|
|
238
|
+
if (isLitLoopDone(plan))
|
|
239
|
+
return "complete";
|
|
240
|
+
return codexGoalMode(plan) === "per_story" ? "complete" : "active";
|
|
241
|
+
}
|
|
221
242
|
// -- record-evidence -----------------------------------------------------------
|
|
222
243
|
const EVIDENCE_STATUSES = new Set(["pass", "fail", "blocked"]);
|
|
223
244
|
export async function handleRecordEvidence(argv, ctx) {
|
|
@@ -31,9 +31,11 @@ export declare function evidencePath(repoRoot: string, name: string, scope?: Lit
|
|
|
31
31
|
export declare function repoRelative(absolutePath: string, repoRoot: string): string;
|
|
32
32
|
/**
|
|
33
33
|
* Resolve a LitLoopScope from argv/env. Precedence (first non-blank wins): `--session <id>` /
|
|
34
|
-
* `--session=<id>`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* `--session=<id>` / `--session-id <id>` / `--session-id=<id>` → env `LITCODEX_SESSION_ID` →
|
|
35
|
+
* `CODEX_SESSION_ID` → `CODEX_THREAD_ID` → compatibility env `LITCODEX_LOOP_SESSION` →
|
|
36
|
+
* `LIT_LOOP_SESSION`. The chosen raw id is normalized; returns `{ sessionId }` when a non-null id
|
|
37
|
+
* results, else `undefined`. Pure aside from reading the provided `env` object (NEVER
|
|
38
|
+
* `process.env`); zero I/O.
|
|
37
39
|
*/
|
|
38
40
|
export declare function resolveLoopScope(source: {
|
|
39
41
|
argv?: readonly string[];
|
|
@@ -87,25 +87,35 @@ export function repoRelative(absolutePath, repoRoot) {
|
|
|
87
87
|
function readSessionFlag(argv) {
|
|
88
88
|
for (let i = 0; i < argv.length; i++) {
|
|
89
89
|
const arg = argv[i];
|
|
90
|
-
if (arg === "--session") {
|
|
90
|
+
if (arg === "--session" || arg === "--session-id") {
|
|
91
91
|
return argv[i + 1];
|
|
92
92
|
}
|
|
93
93
|
if (arg?.startsWith("--session=")) {
|
|
94
94
|
return arg.slice("--session=".length);
|
|
95
95
|
}
|
|
96
|
+
if (arg?.startsWith("--session-id=")) {
|
|
97
|
+
return arg.slice("--session-id=".length);
|
|
98
|
+
}
|
|
96
99
|
}
|
|
97
100
|
return undefined;
|
|
98
101
|
}
|
|
99
102
|
/**
|
|
100
103
|
* Resolve a LitLoopScope from argv/env. Precedence (first non-blank wins): `--session <id>` /
|
|
101
|
-
* `--session=<id>`
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
+
* `--session=<id>` / `--session-id <id>` / `--session-id=<id>` → env `LITCODEX_SESSION_ID` →
|
|
105
|
+
* `CODEX_SESSION_ID` → `CODEX_THREAD_ID` → compatibility env `LITCODEX_LOOP_SESSION` →
|
|
106
|
+
* `LIT_LOOP_SESSION`. The chosen raw id is normalized; returns `{ sessionId }` when a non-null id
|
|
107
|
+
* results, else `undefined`. Pure aside from reading the provided `env` object (NEVER
|
|
108
|
+
* `process.env`); zero I/O.
|
|
104
109
|
*/
|
|
105
110
|
export function resolveLoopScope(source) {
|
|
106
111
|
const argv = source.argv ?? [];
|
|
107
112
|
const env = source.env ?? {};
|
|
108
|
-
const raw = readSessionFlag(argv) ??
|
|
113
|
+
const raw = readSessionFlag(argv) ??
|
|
114
|
+
env["LITCODEX_SESSION_ID"] ??
|
|
115
|
+
env["CODEX_SESSION_ID"] ??
|
|
116
|
+
env["CODEX_THREAD_ID"] ??
|
|
117
|
+
env["LITCODEX_LOOP_SESSION"] ??
|
|
118
|
+
env["LIT_LOOP_SESSION"];
|
|
109
119
|
const sessionId = normalizeSessionId(raw);
|
|
110
120
|
return sessionId === null ? undefined : { sessionId };
|
|
111
121
|
}
|
|
@@ -130,6 +130,14 @@ function planInvalidReason(plan) {
|
|
|
130
130
|
if (!(sessionId === null || typeof sessionId === "string")) {
|
|
131
131
|
return "sessionId must be a string or null";
|
|
132
132
|
}
|
|
133
|
+
const codexGoalMode = field(plan, "codexGoalMode");
|
|
134
|
+
if (codexGoalMode !== undefined && codexGoalMode !== "aggregate" && codexGoalMode !== "per_story") {
|
|
135
|
+
return "codexGoalMode must be aggregate or per_story when present";
|
|
136
|
+
}
|
|
137
|
+
const codexObjective = field(plan, "codexObjective");
|
|
138
|
+
if (codexObjective !== undefined && typeof codexObjective !== "string") {
|
|
139
|
+
return "codexObjective must be a string when present";
|
|
140
|
+
}
|
|
133
141
|
const goals = field(plan, "goals");
|
|
134
142
|
if (!Array.isArray(goals)) {
|
|
135
143
|
return "goals must be an array";
|
|
@@ -6,6 +6,7 @@ export type LitLoopCriterionStatus = "pending" | "pass" | "fail" | "blocked";
|
|
|
6
6
|
* branch + validation mismatch. Re-introducing it is a coordinated M08+M09 change.
|
|
7
7
|
*/
|
|
8
8
|
export type LitLoopUserModel = "happy" | "edge" | "regression";
|
|
9
|
+
export type LitLoopCodexGoalMode = "aggregate" | "per_story";
|
|
9
10
|
/** Frozen tuple of the three user-model values (enumerable, single source). */
|
|
10
11
|
export declare const LIT_LOOP_USER_MODELS: readonly LitLoopUserModel[];
|
|
11
12
|
/** The 13-member ledger event-kind union: M08's 11 ∪ M09's 10 (A3 C7). */
|
|
@@ -49,6 +50,8 @@ export interface LitLoopPlan {
|
|
|
49
50
|
ledgerPath: string;
|
|
50
51
|
evidenceDir: string;
|
|
51
52
|
sessionId: string | null;
|
|
53
|
+
codexGoalMode?: LitLoopCodexGoalMode;
|
|
54
|
+
codexObjective?: string;
|
|
52
55
|
activeGoalId?: string;
|
|
53
56
|
goals: LitLoopGoal[];
|
|
54
57
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litcodex-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Codex loop harness installer. Run `npx litcodex-ai install` to set up the LitCodex Codex platform: the bare `lit` hook and the durable lit-loop runtime.",
|
|
5
5
|
"keywords": ["codex", "litcodex", "lit-loop", "ai-agents", "orchestration"],
|
|
6
6
|
"author": "LitCodex Authors",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": ["bin", "dist", "model-catalog.json", "README.md", "LICENSE"],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@litcodex/lit-loop": "0.3.
|
|
18
|
+
"@litcodex/lit-loop": "0.3.9"
|
|
19
19
|
},
|
|
20
20
|
"bundledDependencies": ["@litcodex/lit-loop"],
|
|
21
21
|
"scripts": {
|