@paleo/alcode 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Run `alcode --guide` for the full delegation guide. When an OpenClaw agent is th
8
8
 
9
9
  `alcode` runs the coding agent as a direct **foreground** child of its own process: it streams a live transcript to stdout and to a per-run session file (with a YAML frontmatter status lifecycle `running` → `succeeded`/`failed`), and blocks until the agent exits. It never backgrounds or detaches itself.
10
10
 
11
- Coding runs can be very long: the caller always runs `alcode` as a background task and owns the backgrounding. Under OpenClaw the agent invokes `alcode` through the `exec` tool with `background: true` and `timeout: 0`. OpenClaw wakes the agent on exit (via `tools.exec.notifyOnExit`), which then reads the session file for the result. This keeps the run's lifecycle owned by one supervisor (the caller) instead of a detached process phoning back in. The session file is the durable result handoff: frontmatter `sessionId` + status, and the `---- Result ----` block.
11
+ Coding runs can be very long: the caller always runs `alcode` as a background task and owns the backgrounding. Under OpenClaw the agent invokes `alcode` through the `exec` tool with `background: true` and `timeout: 0`, chaining `openclaw system event --mode now --session-key <key>` onto the command as prescribed by the guide; that immediate wake fires when the run finishes, and the woken agent reads the session file for the result. This keeps the run's lifecycle owned by one supervisor (the caller) instead of a detached process phoning back in. The session file is the durable result handoff: frontmatter `sessionId` + status, and the `---- Result ----` block.
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
 
package/dist/cli.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface MainOptions {
9
9
  }
10
10
  export declare function main(options?: MainOptions): Promise<number>;
11
11
  export declare function checkLaunchGuards(parsed: AlcodeArgs, realCwd: string, records: SessionRecord[]): string | undefined;
12
+ export declare function resolveTicket(parsed: AlcodeArgs, records: SessionRecord[]): string | undefined;
12
13
  export declare function buildRunConfig(parsed: AlcodeArgs, cwd: string, sessionFilePath: string, env: NodeJS.ProcessEnv): RunConfig;
13
14
  export interface AlcodeArgs {
14
15
  isNew: boolean;
package/dist/cli.js CHANGED
@@ -58,14 +58,16 @@ async function runSession(parsed, ctx) {
58
58
  return 1;
59
59
  }
60
60
  const realCwd = realpathSync(cwd);
61
- const guardError = checkLaunchGuards(parsed, realCwd, listSessionRecords(cwd));
61
+ const records = listSessionRecords(cwd);
62
+ const guardError = checkLaunchGuards(parsed, realCwd, records);
62
63
  if (guardError) {
63
64
  stderr.write(`${guardError}\n`);
64
65
  return 1;
65
66
  }
66
67
  const now = new Date();
67
- const sessionFilePath = resolveSessionFilePath(cwd, parsed.ticket, now);
68
- writeInitialSessionFile(sessionFilePath, buildFrontmatter(parsed, now, realCwd));
68
+ const ticket = resolveTicket(parsed, records);
69
+ const sessionFilePath = resolveSessionFilePath(cwd, ticket, now);
70
+ writeInitialSessionFile(sessionFilePath, buildFrontmatter(parsed, now, realCwd, ticket));
69
71
  stdout.write(`Session file: ${relative(cwd, sessionFilePath)}\n\n`);
70
72
  const result = await runClaude(buildRunConfig(parsed, cwd, sessionFilePath, env), stdout);
71
73
  if (parsed.isNew && result.sessionId) {
@@ -110,11 +112,43 @@ function unknownResumeError(resume, records) {
110
112
  });
111
113
  return `Error: unknown session id ${resume}. Known recent sessions:\n${recent.join("\n")}`;
112
114
  }
113
- function buildFrontmatter(parsed, now, realCwd) {
115
+ // The effective ticket scopes the session file to `.plans/<ticket>/_alcode/` and lands in the
116
+ // frontmatter. Exported for tests. Precedence: explicit `--ticket`, then the resumed session's
117
+ // records, then a `.plans/<ticket>/` path in the message.
118
+ export function resolveTicket(parsed, records) {
119
+ if (parsed.ticket !== undefined)
120
+ return parsed.ticket;
121
+ if (parsed.resume !== undefined)
122
+ return inheritTicketFromResume(parsed.resume, records);
123
+ if (parsed.isNew && parsed.message !== undefined)
124
+ return inferTicketFromMessage(parsed.message);
125
+ return;
126
+ }
127
+ // Latest record of the resumed session that carries a ticket — the resumed run keeps writing
128
+ // under the same ticket directory, and its frontmatter keeps the ticket for later resumes.
129
+ function inheritTicketFromResume(resume, records) {
130
+ const ticketed = records.filter((r) => r.frontmatter.sessionId === resume && r.frontmatter.ticket !== null);
131
+ const latest = ticketed.sort((a, b) => b.frontmatter.startedAt.localeCompare(a.frontmatter.startedAt))[0];
132
+ return latest?.frontmatter.ticket ?? undefined;
133
+ }
134
+ // A message like `Execute the plan: .plans/2/B2-plan.md` names its ticket. `_`-prefixed segments
135
+ // (e.g. `_alcode`) are not tickets; several distinct candidates mean the message is ambiguous.
136
+ function inferTicketFromMessage(message) {
137
+ const candidates = new Set();
138
+ for (const match of message.matchAll(/\.plans\/([A-Za-z0-9._-]+)\//g)) {
139
+ const segment = match[1];
140
+ if (!segment.startsWith("_") && isPathSafeTicket(segment))
141
+ candidates.add(segment);
142
+ }
143
+ if (candidates.size !== 1)
144
+ return;
145
+ return [...candidates][0];
146
+ }
147
+ function buildFrontmatter(parsed, now, realCwd, ticket) {
114
148
  return {
115
149
  status: "running",
116
150
  protocol: parsed.protocol ?? null,
117
- ticket: parsed.ticket ?? null,
151
+ ticket: ticket ?? null,
118
152
  model: parsed.model ?? null,
119
153
  sessionId: null,
120
154
  command: formatCommand(parsed),
@@ -207,9 +241,6 @@ export function validateArgs(args) {
207
241
  if (["spec", "aad"].includes(args.protocol ?? "") && !args.message) {
208
242
  return `Error: --protocol ${args.protocol} requires --message.`;
209
243
  }
210
- if (args.ticket !== undefined && !args.isNew) {
211
- return "Error: --ticket is only valid with --new.";
212
- }
213
244
  if (args.ticket !== undefined && !isPathSafeTicket(args.ticket)) {
214
245
  return ("Error: --ticket must be a single path segment " +
215
246
  "(letters, digits, '.', '-', '_'); no path separators or '..'.");
package/dist/guide.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
- // templates/guide.md is the shared skeleton; the per-variant fragments fill its tags:
3
- // {{TITLE-SUFFIX}} appended to the H1 to label the variant
4
- // {{RUN}} how to background the run on the caller's platform
5
- // {{WAKE}} how the completion wake arrives, introducing the shared steps
2
+ // Each variant owns a full guide (templates/<variant>-guide.md) carrying its own
3
+ // platform-specific prose. Two shared blocks fill the tags common to every variant:
4
+ // {{INTRODUCTION}} what alcode is and how to invoke it
5
+ // {{CLI_REFERENCE}} — the CLI reference and the protocol workflows below it
6
6
  export function renderGuide(variant) {
7
- return readTemplate("guide.md")
8
- .replaceAll("{{TITLE-SUFFIX}}", variant === "openclaw" ? " (OpenClaw)" : "")
9
- .replaceAll("{{RUN}}", readTemplate(`guide-run-${variant}.md`).trimEnd())
10
- .replaceAll("{{WAKE}}", readTemplate(`guide-wake-${variant}.md`).trimEnd())
7
+ return readTemplate(`${variant}-guide.md`)
8
+ .replaceAll("{{INTRODUCTION}}", readTemplate("introduction.md").trimEnd())
9
+ .replaceAll("{{CLI_REFERENCE}}", readTemplate("cli-reference.md").trimEnd())
11
10
  .trimEnd();
12
11
  }
13
12
  function readTemplate(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/alcode",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
6
  "description": "Run a coding agent through AlignFirst protocols, with a durable session file per run.",
@@ -1,35 +1,3 @@
1
- # AlignFirst Delegation Guide{{TITLE-SUFFIX}}
2
-
3
- Run a coding agent through AlignFirst protocols with the `alcode` CLI. It wraps a coding-agent CLI for non-interactive use: it invokes a protocol, streams the run to a session file, and returns the result.
4
-
5
- **Never implement, investigate, or modify the codebase yourself while delegating. Your role is to delegate and guide the agent.**
6
-
7
- Run `alcode` from the root of the target project, so the agent works in the right repo. The project must contain a `.plans/` directory.
8
-
9
- ## How it runs
10
-
11
- `alcode` runs the coding agent in the **foreground** and blocks until it finishes, streaming the transcript to stdout as it arrives. It never backgrounds or detaches itself.
12
-
13
- Coding runs can be long (several hours is fine): **always run `alcode` as a background task**, so you stay free while it works. The backgrounding is **your platform's** job, never alcode's own.
14
-
15
- {{RUN}}
16
-
17
- As soon as the run is backgrounded, tell the user the coding agent is now working in the background and that you will report back when it finishes (e.g. *"Le coding agent tourne en arrière-plan — je te préviens dès que c'est terminé."*). Then go available. Do **not** poll.
18
-
19
- 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.
20
-
21
- **One protocol run at a time per workspace** — protocol runs share the working tree. Finish (or kill) the current protocol run before launching or resuming another. Plain messages (answers, questions) can be sent at any time.
22
-
23
- ## After a background run completes
24
-
25
- {{WAKE}}
26
-
27
- 1. **Read the run's session file** (the path `alcode` printed on its first line, under `_alcode/`). Its frontmatter holds `status` (`succeeded` / `failed`) and the session id; the `---- Result ----` block holds the outcome. If you set `meta` at launch, it is there too.
28
- 2. **Report the outcome to the user** where the work was requested. Send one concise message: succeeded or failed, plus a one-line summary of the result for the audience. If the frontmatter `meta` carries a destination (e.g. a thread target), route this report there — a plain reply from the wake turn goes to the session's default surface, which may not be where the work was requested. When the report went out through the `message` tool, end the wake turn with a final answer of exactly `NO_REPLY` — any other final text streams to the default surface as a stray duplicate. `NO_REPLY` is the only silent ending; never improvise another token (`HEARTBEAT_OK` posts as literal text).
29
- 3. Do **not** re-verify the repo, re-run the coding agent, fetch/merge branches, or inspect `git`. The coding agent already did the work and the session file is authoritative. Relay its outcome, nothing more.
30
-
31
- 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.
32
-
33
1
  ## CLI reference
34
2
 
35
3
  ```
@@ -44,7 +12,7 @@ alcode --resume <sessionId> [--protocol <protocol>] [--message "..."]
44
12
  | `--resume <id>` | Continue an existing session. |
45
13
  | `--protocol <p>` | One of `spec`, `plan`, `aad`, `description`, `read`, `review`, `merge`. Optional. |
46
14
  | `--ticket <id>` | Ticket ID. Required with `--new` + `--protocol`. |
47
- | `--message "..."` | Message to send. Required for `spec`, `aad`, and when no `--protocol`. |
15
+ | `--message "..."` | Message to send, written in English. Required for `spec`, `aad`, and when no `--protocol`. |
48
16
  | `--model <model>` | Model override. |
49
17
  | `--meta "..."` | Opaque handoff string stored verbatim in the session file's `meta:` frontmatter. `alcode` never reads it — it's for you to stash context the run's later reader needs (e.g. where to report the outcome). |
50
18
 
@@ -89,7 +57,7 @@ Answer questions as in the spec flow. The agent implements and writes a summary
89
57
 
90
58
  ## Answering agent questions
91
59
 
92
- During spec and AAD sessions the agent asks questions before proceeding. Resume **without a protocol** to answer. Answer all questions in one message, numbered to match:
60
+ During spec and AAD sessions the agent asks questions before proceeding. Resume **without a protocol** to answer. Compose the answers in English, all questions in one message, numbered to match:
93
61
 
94
62
  ```bash
95
63
  alcode --resume <sessionId> --message \
@@ -0,0 +1,19 @@
1
+ # AlignFirst Delegation Guide
2
+
3
+ {{INTRODUCTION}}
4
+
5
+ ## How it runs
6
+
7
+ `alcode` runs the coding agent in the **foreground** and blocks until it finishes, streaming the transcript to stdout and to a session file under `.plans/`. It never backgrounds or detaches itself.
8
+
9
+ Coding runs can be long (several hours is fine): background `alcode` with your platform's own background-execution facility — never detach it with `&` or a detach wrapper. **One protocol run at a time per worktree**; plain messages can be sent at any time.
10
+
11
+ Running under OpenClaw? Read `alcode --openclaw-guide` instead — the same manual with the OpenClaw-specific run and wake instructions.
12
+
13
+ ## After a run completes
14
+
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
+
17
+ 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
+
19
+ {{CLI_REFERENCE}}
@@ -0,0 +1,5 @@
1
+ Run a coding agent through AlignFirst protocols with the `alcode` CLI. It wraps a coding-agent CLI for non-interactive use: it invokes a protocol, streams the run to a session file, and returns the result.
2
+
3
+ **Never implement, investigate, or modify the codebase yourself while delegating. Your role is to delegate and guide the agent.**
4
+
5
+ Run `alcode` from the root of the target project, so the agent works in the right repo. The project must contain a `.plans/` directory.
@@ -0,0 +1,48 @@
1
+ # AlignFirst Delegation Guide (OpenClaw)
2
+
3
+ {{INTRODUCTION}}
4
+
5
+ ## How it runs
6
+
7
+ `alcode` runs the coding agent in the **foreground** and blocks until it finishes, streaming the transcript to stdout as it arrives. It never backgrounds or detaches itself.
8
+
9
+ Coding runs can be long (several hours is fine): **always run `alcode` as a background task**, so you stay free while it works. The backgrounding is **your platform's** job, never alcode's own.
10
+
11
+ Under OpenClaw, background it through the `exec` tool:
12
+
13
+ - Before the first `alcode` run of this session, call the `session_status` tool and read the `Session:` line from its result — that is this session's key. Obtain it once, reuse it for every run of this session.
14
+ - The exec command chains a completion wake onto the run:
15
+
16
+ `alcode <flags> ; openclaw system event --text "alcode run finished — read its session file and report to the user" --mode now --session-key <KEY>`
17
+
18
+ Chain with `;` (never `&&`) so a failed run wakes you too. The wake may reach you as a bare heartbeat with the text dropped, and OpenClaw's own `Exec completed` notice may lag behind it — never wait for either text.
19
+ - Pass `background: true` and `timeout: 0` (no kill timer). Never rely on the auto-yield or a finite timeout.
20
+ - Set the exec `workdir` to the project root as an **absolute** path (`~` is not expanded there), or `cd` into the project inside the command itself.
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
+ - `--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
+
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."*). Then go available. Do **not** poll.
25
+
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
+
28
+ **One protocol run at a time per workspace** — protocol runs share the working tree. Finish (or kill) the current protocol run before launching or resuming another. Plain messages (answers, questions) can be sent at any time.
29
+
30
+ ## After a background run completes
31
+
32
+ The chained wake fires when the backgrounded `alcode` exits: this session receives a heartbeat, usually as a plain heartbeat poll with no message text (the wake text is often dropped in transit). A run counts as pending while it is running **and until its outcome is reported**.
33
+
34
+ **First, decide whether this heartbeat needs a report.** A heartbeat is the completion wake only while a run is pending. Once you have already reported a run's outcome, later heartbeats for it — a single run can wake you more than once, since the platform's own exec-exit notice may fire on top of the chained wake — need nothing: end the turn with a final answer of exactly `NO_REPLY`. Never `HEARTBEAT_OK`: it is not swallowed, it posts as literal text where the user reads. Whenever a heartbeat turn has nothing new to say, the answer is `NO_REPLY`, never `HEARTBEAT_OK`.
35
+
36
+ Any heartbeat received while an `alcode` run is **still pending** (running, or finished but not yet reported) is the completion wake — do exactly this:
37
+
38
+ 1. **Read the run's session file** (the path `alcode` printed on its first line, under `_alcode/`). Its frontmatter holds `status` (`succeeded` / `failed`) and the session id; the `---- Result ----` block holds the outcome. If you set `meta` at launch, it is there too.
39
+ 2. **Report the outcome to the user** where the work was requested. Send one concise message: succeeded or failed, plus a one-line summary of the result for the audience. Two cases:
40
+ - The frontmatter `meta` carries a destination (a Discord thread target): send the report there with `message` `action: "thread-reply"` (never `action: "send"`), then end the wake turn with a final answer of exactly `NO_REPLY` — any other final text streams to the channel as a stray duplicate.
41
+ - No `meta` destination (always the case on Slack, and on a thread-bound Discord session): the work was requested on this session's own surface, so **the report itself is your plain-text final reply** — just write it, no `message` tool. On Slack it auto-threads back to the right thread. Never end such a wake turn silently.
42
+
43
+ A wake turn ends with the report or with exactly `NO_REPLY` — nothing else, ever, and never `HEARTBEAT_OK`. `NO_REPLY` is the ending after a `message`-tool report (the report is already delivered) and for any wake with nothing new to report.
44
+ 3. Do **not** re-verify the repo, re-run the coding agent, fetch/merge branches, or inspect `git`. The coding agent already did the work and the session file is authoritative. Relay its outcome, nothing more.
45
+
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
+
48
+ {{CLI_REFERENCE}}
@@ -1,3 +0,0 @@
1
- Use your platform's background-execution facility; do not detach `alcode` with `&` or a detach wrapper.
2
-
3
- Running under OpenClaw? Read `alcode --openclaw-guide` instead. It is the same manual with the OpenClaw-specific run instructions.
@@ -1,6 +0,0 @@
1
- Under OpenClaw, background it through the `exec` tool:
2
-
3
- - Pass `background: true` and `timeout: 0` (no kill timer). Never rely on the auto-yield or a finite timeout.
4
- - Set the exec `workdir` to the project root as an **absolute** path (`~` is not expanded there), or `cd` into the project inside the command itself.
5
- - 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.
6
- - If the outcome must be reported into a thread **you created yourself** via `message` `action: "thread-create"`, add `--meta "<THREAD_ID>"` with that thread's `chat_id`: the completion wake's plain reply goes to this session's default surface (the channel), and only the session file's `meta` can point your report back at the thread (via `message` `action: "thread-reply"`). In every other case omit `--meta` — platform-threaded replies and thread-bound sessions already report in the right place.
@@ -1 +0,0 @@
1
- When your platform tells you the backgrounded `alcode` exited, do exactly this:
@@ -1 +0,0 @@
1
- OpenClaw wakes this session when the backgrounded `alcode` exits. The wake may arrive as a **plain heartbeat poll with no message text**. Do not wait for an explicit "exec finished" notice. Whenever you are woken, or receive any heartbeat, while an `alcode` run is pending, do exactly this: