@paleo/alcode 0.5.2 → 0.6.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/README.md CHANGED
@@ -12,6 +12,8 @@ Coding runs can be very long: the caller always runs `alcode` as a background ta
12
12
 
13
13
  If `alcode` is terminated, its signal handlers seal the session file (`status: failed`, `exitReason: terminated`) so it never stays frozen at `running`, then send `SIGTERM` to the coding-agent child, giving it a short grace to tear down its own subprocesses before a `SIGKILL` backstop guarantees no orphan is left behind. Only a `SIGKILL` of `alcode` itself (uncatchable) can leave a stale `running` status.
14
14
 
15
+ When the coding agent's own session on the host is missing or expired, `alcode` detects the `authentication_failed` signal in its stream, seals the session file with `exitReason: auth_required`, and exits `2` (distinct from `1`) with a one-line stderr message.
16
+
15
17
  ## Usage
16
18
 
17
19
  ```bash
package/dist/cli.js CHANGED
@@ -5,6 +5,9 @@ import { renderGuide } from "./guide.js";
5
5
  import { assertPlansGate, listSessionRecords, resolveSessionFilePath, writeInitialSessionFile, } from "./session-file.js";
6
6
  import { buildPrompt, PROTOCOLS } from "./prompt.js";
7
7
  import { runClaude } from "./run-claude.js";
8
+ // Distinct from 1 (ordinary run failure) so a script can branch on an auth failure that needs an
9
+ // operator re-login rather than a retry.
10
+ const EXIT_AUTH_REQUIRED = 2;
8
11
  export async function main(options) {
9
12
  const argv = options?.argv ?? process.argv;
10
13
  const stdout = options?.stdout ?? process.stdout;
@@ -73,6 +76,11 @@ async function runSession(parsed, ctx) {
73
76
  if (parsed.isNew && result.sessionId) {
74
77
  stdout.write(`\nSession ID: ${result.sessionId}\n`);
75
78
  }
79
+ if (result.authRequired) {
80
+ stderr.write("alcode: coding agent not authenticated — an administrator must re-login on the host " +
81
+ "(`claude`, then `/login`).\n");
82
+ return EXIT_AUTH_REQUIRED;
83
+ }
76
84
  return result.status === "succeeded" ? 0 : 1;
77
85
  }
78
86
  // Fail-fast launch guards, run against the (healed) session records before anything is written.
@@ -15,6 +15,7 @@ export interface RunOutput {
15
15
  }
16
16
  export interface RunResult {
17
17
  status: "succeeded" | "failed";
18
+ authRequired: boolean;
18
19
  sessionId: string | null;
19
20
  result: string;
20
21
  }
@@ -26,6 +27,7 @@ export interface StreamState {
26
27
  sessionId?: string;
27
28
  result?: string;
28
29
  isError: boolean;
30
+ authFailed: boolean;
29
31
  }
30
32
  export declare function createStreamState(): StreamState;
31
33
  export declare function parseEventLine(line: string): unknown;
@@ -11,15 +11,25 @@ export async function runClaude(config, out) {
11
11
  const state = createStreamState();
12
12
  const outcome = await spawnClaude(config, state, out);
13
13
  const failed = state.isError || outcome.exitCode !== 0 || state.result === undefined;
14
- const result = state.result ?? failureMessage(outcome);
14
+ // An auth signal only matters when the run actually failed: claude retries a 401, so a signal it
15
+ // recovered from (run still succeeded) is not a problem to report.
16
+ const authRequired = failed && state.authFailed;
17
+ const result = authRequired
18
+ ? authFailureMessage(state.result)
19
+ : (state.result ?? failureMessage(outcome));
15
20
  applyCompletion(config.sessionFilePath, {
16
21
  status: failed ? "failed" : "succeeded",
17
22
  endedAt: new Date().toISOString(),
18
- exitReason: failed ? "error" : "completed",
23
+ exitReason: authRequired ? "auth_required" : failed ? "error" : "completed",
19
24
  sessionId: state.sessionId ?? null,
20
25
  result,
21
26
  });
22
- return { status: failed ? "failed" : "succeeded", sessionId: state.sessionId ?? null, result };
27
+ return {
28
+ status: failed ? "failed" : "succeeded",
29
+ authRequired,
30
+ sessionId: state.sessionId ?? null,
31
+ result,
32
+ };
23
33
  }
24
34
  function spawnClaude(config, state, out) {
25
35
  const child = spawn("claude", buildClaudeArgs(config), {
@@ -167,7 +177,7 @@ function emitEvent(sessionFilePath, line, state, out) {
167
177
  out.write(`${rendered}\n`);
168
178
  }
169
179
  export function createStreamState() {
170
- return { isError: false };
180
+ return { isError: false, authFailed: false };
171
181
  }
172
182
  export function parseEventLine(line) {
173
183
  try {
@@ -183,6 +193,7 @@ export function renderEvent(event, state) {
183
193
  if (!isRecord(event))
184
194
  return;
185
195
  captureSessionId(event, state);
196
+ captureAuthFailure(event, state);
186
197
  switch (event.type) {
187
198
  case "system":
188
199
  return event.subtype === "init" ? `[init] session ${asString(event.session_id)}` : undefined;
@@ -204,6 +215,10 @@ function captureSessionId(event, state) {
204
215
  if (id)
205
216
  state.sessionId = id;
206
217
  }
218
+ function captureAuthFailure(event, state) {
219
+ if (event.error === "authentication_failed")
220
+ state.authFailed = true;
221
+ }
207
222
  function captureResult(event, state) {
208
223
  state.isError = event.is_error === true;
209
224
  const result = asString(event.result);
@@ -248,6 +263,15 @@ function failureMessage(outcome) {
248
263
  const stderr = outcome.stderr.trim();
249
264
  return stderr || `claude exited with code ${outcome.exitCode ?? "unknown"}`;
250
265
  }
266
+ // The remedy is an operator action on the host, not a retry — so the result block names it and
267
+ // keeps claude's own reason line (e.g. "OAuth session expired…") beneath it.
268
+ function authFailureMessage(claudeReason) {
269
+ const base = "Coding agent not authenticated (authentication_failed): the host session is missing, expired, " +
270
+ "or rejected. An administrator must re-login on the host (run `claude`, then `/login`) before " +
271
+ "alcode can run again.";
272
+ const detail = claudeReason?.trim();
273
+ return detail ? `${base}\n\n${detail}` : base;
274
+ }
251
275
  // --- Shared helpers ---
252
276
  function isRecord(value) {
253
277
  return typeof value === "object" && value !== null;
@@ -19,7 +19,7 @@ export declare function writeInitialSessionFile(sessionFilePath: string, frontma
19
19
  export interface CompletionUpdate {
20
20
  status: "succeeded" | "failed";
21
21
  endedAt: string;
22
- exitReason: "completed" | "error" | "terminated";
22
+ exitReason: "completed" | "error" | "terminated" | "auth_required";
23
23
  sessionId: string | null;
24
24
  result: string;
25
25
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@paleo/alcode",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
- "description": "Run a coding agent through AlignFirst protocols, with a durable session file per run.",
6
+ "description": "Run a coding agent through AlignFirst protocols.",
7
7
  "keywords": [
8
8
  "ai",
9
9
  "agent",
@@ -35,7 +35,7 @@ For large work, do not rush. Decompose it yourself only when the concerns are tr
35
35
 
36
36
  1. **Spec** — `alcode --new --protocol spec --ticket AB-123 --message "Feature description"`. The agent investigates and asks questions; save the session id. Iterate until it writes the spec file.
37
37
  2. **Plan** — `alcode --resume <sessionId> --protocol plan`. The agent writes the plan file.
38
- 3. **Execute** — `alcode --new --message "Execute the plan: \`.plans/AB-123/A2-plan.md\`"`. The agent implements and writes a summary file.
38
+ 3. **Execute** — `alcode --new --message "Execute the plan: \`.plans/AB-123/A2-plan.md\`"`. The agent implements and writes a summary file. When executing a main plan that spans several sub-plans and the working tree is clean, append to the message: *"Feel free to commit between each plan."*
39
39
  4. **Commit** — use the suggested commit message from the spec file.
40
40
 
41
41
  ## Light workflow (AAD)
@@ -14,6 +14,8 @@ Running under OpenClaw? Read `alcode --openclaw-guide` instead — the same manu
14
14
 
15
15
  Read the run's session file (the path `alcode` prints on its first line, under `_alcode/`): its frontmatter carries `status` (`succeeded`/`failed`) and the `sessionId`, and the `---- Result ----` block holds the outcome. Report it to the user where the work was requested. If the run failed, say so plainly and propose the next step.
16
16
 
17
+ An `exitReason` of `auth_required` in the frontmatter (alcode also exits `2`) means the coding agent is not authenticated on the host: an administrator must re-login there before any run can succeed. Report that and do not retry.
18
+
17
19
  Do **not** re-verify the repo, re-run the agent, or inspect `git` — the session file is authoritative. Keep session files in place; they are the durable audit trail.
18
20
 
19
21
  {{CLI_REFERENCE}}
@@ -21,7 +21,7 @@ Under OpenClaw, background it through the `exec` tool:
21
21
  - The session-file path comes from the run's first stdout line (`Session file: …`), available via `process log <id>`. The stamp in the file name is the run's start time; it cannot be derived from the clock.
22
22
  - `--meta` is for one case only: a **Discord** thread **you created yourself** this turn via `message` `action: "thread-create"`. There, the completion wake's plain reply would go to the channel, so add `--meta "<THREAD_ID>"` with that thread's `chat_id`; the wake then reports back into the thread via `message` `action: "thread-reply"`. In **every other case — always on Slack, and on Discord when the session is already thread-bound — omit `--meta`.** Slack auto-threads plain replies and exposes no `send`/`thread-reply` action, so a `--meta` there makes the wake attempt an unsupported `message` send that fails; a thread-bound session already replies in its own thread.
23
23
 
24
- As soon as the run is backgrounded, tell the user — in the user's language — that the coding agent is now working in the background and that you will report back when it finishes (e.g. *"The coding agent is running in the background — I'll let you know as soon as it's done."*). On Discord this "started" ack, like every user-facing post in the thread, is a `message` `action: "thread-reply"` carrying the thread's `threadId` — free-form assistant text streams to the parent channel, not the thread, so never emit this ack as plain prose. Then go available. Do **not** poll.
24
+ As soon as the run is backgrounded, tell the user — in the user's language — that the coding agent is now working in the background and that you will report back when it finishes (e.g. *"The coding agent is running in the background — I'll let you know as soon as it's done."*). Deliver this "started" ack exactly like the completion report below: if you set `--meta` (a Discord thread you created this turn), the ack is a `message` `action: "thread-reply"` carrying that thread's `threadId` — your free-form text would stream to the parent channel. In every other case — always on Slack, and on a thread-bound Discord session the ack is your plain text; do **not** also post it via `message`, that double-posts. Then go available. Do **not** poll.
25
25
 
26
26
  Every run writes a session file under `.plans/`: `.plans/<ticket>/_alcode/<stamp>.md`, or `.plans/_alcode/<stamp>.md` without a ticket. This file is the durable record of the run. Its frontmatter carries `status` (`running` → `succeeded`/`failed`) and the `sessionId`, and the `---- Result ----` block holds the outcome.
27
27
 
@@ -45,4 +45,6 @@ Any heartbeat received while an `alcode` run is **still pending** (running, or f
45
45
 
46
46
  If the session file says the run failed, report that plainly and propose the next step; don't silently retry. When a session turns bad, keep everything in place — session files, directories, and records are the durable audit trail; never delete them; just start a new session.
47
47
 
48
+ If the frontmatter's `exitReason` is `auth_required`, the coding agent itself is not authenticated on the host (its session is missing or expired). No run can succeed until an administrator re-logs it in on the host. Tell the user exactly that, and do not retry — a retry hits the same wall.
49
+
48
50
  {{CLI_REFERENCE}}