litcodex-ai 0.3.8 → 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.
@@ -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
- if (!WELL_FORMED_HEADER.test(raw)) {
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
- Record everything to `.litcodex/drafts/<slug>.md` as you go: confirmed
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. Write ONE plan to `.litcodex/plans/<slug>.md` using the template below. No
154
- "Phase 1 plan / Phase 2 plan" splits; 50+ todos is fine. Build it
155
- incrementally skeleton first, then append todo batches — so output limits
156
- never truncate it; re-read the file with the `shell` tool to confirm
157
- completeness.
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
- Treat TOML-backed role routing as **routing-unverified**. The
247
- `multi_agent_v1.spawn_agent` schema accepts `message`, `fork_context`,
248
- `agent_type`, and `model`; it cannot select a TOML-backed role, model, reasoning
249
- effort, or `service_tier` by name alone. Say so briefly in the draft, paste the
250
- role requirements into the message, and judge the result from delivered
251
- evidence.
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 active lit-loop goal. Per-story model only (one Codex goal per
6
- // lit-loop goal). Pure — no I/O, no store, no clock.
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
- "- First call get_goal. If no active goal exists, call create_goal with the payload below.",
30
- "- If get_goal already reports this objective as active, continue without creating a new goal.",
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 ["- This is not the final lit-loop goal; leave the Codex goal active for the next run."];
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
- /** 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;
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
- /** 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;
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) {
@@ -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: { ok: true, goal, ledgerEntry, codexGoal: { status }, plan, summary: summarizePlan(plan) },
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>` env `LITCODEX_LOOP_SESSION` → env `LIT_LOOP_SESSION`. The chosen raw id is
35
- * normalized; returns `{ sessionId }` when a non-null id results, else `undefined`. Pure aside
36
- * from reading the provided `env` object (NEVER `process.env`); zero I/O.
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>` env `LITCODEX_LOOP_SESSION` → env `LIT_LOOP_SESSION`. The chosen raw id is
102
- * normalized; returns `{ sessionId }` when a non-null id results, else `undefined`. Pure aside
103
- * from reading the provided `env` object (NEVER `process.env`); zero I/O.
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) ?? env["LITCODEX_LOOP_SESSION"] ?? env["LIT_LOOP_SESSION"];
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litcodex/lit-loop",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "LitCodex Lit-Loop runtime: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litcodex-ai",
3
- "version": "0.3.8",
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.8"
18
+ "@litcodex/lit-loop": "0.3.9"
19
19
  },
20
20
  "bundledDependencies": ["@litcodex/lit-loop"],
21
21
  "scripts": {