aligndev 0.0.0 → 0.19.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 +166 -1
- package/bin/aligndev.mjs +3 -0
- package/dist/alignfirst-cli.d.ts +10 -0
- package/dist/alignfirst-cli.js +56 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +95 -0
- package/dist/code/claude-agent.d.ts +14 -0
- package/dist/code/claude-agent.js +168 -0
- package/dist/code/code-cli.d.ts +72 -0
- package/dist/code/code-cli.js +630 -0
- package/dist/code/codex-agent.d.ts +15 -0
- package/dist/code/codex-agent.js +168 -0
- package/dist/code/codex-rollout.d.ts +18 -0
- package/dist/code/codex-rollout.js +114 -0
- package/dist/code/coding-agent.d.ts +4 -0
- package/dist/code/coding-agent.js +6 -0
- package/dist/code/models.d.ts +14 -0
- package/dist/code/models.js +89 -0
- package/dist/code/prompt.d.ts +11 -0
- package/dist/code/prompt.js +26 -0
- package/dist/code/quota.d.ts +25 -0
- package/dist/code/quota.js +248 -0
- package/dist/code/run-agent.d.ts +60 -0
- package/dist/code/run-agent.js +212 -0
- package/dist/code/session-file.d.ts +50 -0
- package/dist/code/session-file.js +263 -0
- package/dist/command-form.d.ts +6 -0
- package/dist/command-form.js +7 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +72 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +6 -0
- package/dist/guide/code-guide.d.ts +4 -0
- package/dist/guide/code-guide.js +17 -0
- package/dist/guide/guide-cli.d.ts +3 -0
- package/dist/guide/guide-cli.js +81 -0
- package/dist/guide/render-template.d.ts +4 -0
- package/dist/guide/render-template.js +62 -0
- package/dist/guide/topics.d.ts +3 -0
- package/dist/guide/topics.js +16 -0
- package/dist/output.d.ts +3 -0
- package/dist/output.js +1 -0
- package/dist/project/discovery.d.ts +42 -0
- package/dist/project/discovery.js +287 -0
- package/dist/project/format.d.ts +6 -0
- package/dist/project/format.js +31 -0
- package/dist/project/guide.d.ts +3 -0
- package/dist/project/guide.js +69 -0
- package/dist/project/layout.d.ts +36 -0
- package/dist/project/layout.js +128 -0
- package/dist/project/markers.d.ts +18 -0
- package/dist/project/markers.js +90 -0
- package/dist/project/ports.d.ts +3 -0
- package/dist/project/ports.js +55 -0
- package/dist/project/project-cli.d.ts +17 -0
- package/dist/project/project-cli.js +227 -0
- package/dist/project/render.d.ts +10 -0
- package/dist/project/render.js +144 -0
- package/dist/project/status.d.ts +24 -0
- package/dist/project/status.js +110 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +5 -0
- package/package.json +38 -3
- package/templates/guide/code.md +252 -0
- package/templates/guide/playbook/channel-handling.md +110 -0
- package/templates/guide/playbook/consultation.md +71 -0
- package/templates/guide/playbook/discord-message-tool.md +37 -0
- package/templates/guide/playbook/playbook.md +139 -0
- package/templates/guide/playbook/project-lifecycle.md +100 -0
- package/templates/guide/playbook/project-workspace-setup.md +186 -0
- package/templates/guide/playbook/slack-message-tool.md +23 -0
- package/templates/guide/playbook/working-session.md +588 -0
- package/templates/guide/project.md +33 -0
package/README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
|
1
1
|
# aligndev
|
|
2
2
|
|
|
3
|
-
The AlignFirst Dev Kit CLI.
|
|
3
|
+
The AlignFirst Dev Kit CLI. It carries the assistant's playbook, runs a coding agent through [AlignFirst](https://github.com/paleo/alignfirst) protocols, and keeps the inventory of the host's projects and their port ranges. The assistant is an OpenClaw bot, or a coding agent working on one project.
|
|
4
|
+
|
|
5
|
+
Run `aligndev` through `npx` (`npx -y aligndev …`), or install it with `npm install -g aligndev`. Through `npx`, it runs `alignfirst` through `npx` too, and its help and guides print both commands in that form. A global `aligndev` needs the `alignfirst` CLI on `PATH`: `npm install -g alignfirst`.
|
|
6
|
+
|
|
7
|
+
Supported systems: Linux and macOS, and Windows through WSL.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
aligndev code <command> [<options>] # run a coding agent through AlignFirst protocols
|
|
13
|
+
aligndev project <command> [<options>] # list projects, check the inventory, claim port ranges
|
|
14
|
+
aligndev guide [<topic>] # print the playbook and the guides
|
|
15
|
+
aligndev --help
|
|
16
|
+
aligndev --version
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each command prints its own usage with `--help`.
|
|
20
|
+
|
|
21
|
+
### `aligndev code`
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
aligndev code new --protocol spec --ticket AB-123 --message "Feature description"
|
|
25
|
+
aligndev code resume <sessionId> --protocol plan
|
|
26
|
+
aligndev code new --message "Execute the plan: .plans/AB-123/A2-plan.md"
|
|
27
|
+
aligndev code new --protocol aad --no-ticket --message "Task description"
|
|
28
|
+
aligndev code new --ticket AB-123 --catchup --protocol aad --message-file message.md
|
|
29
|
+
aligndev code status .plans/AB-123/_aligndev/20260829-135529.md
|
|
30
|
+
aligndev code quota
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The coding agent `aligndev code` launches is **the coder**. Run `aligndev code` from the root of the target project. The project must have a `.plans/` directory, in its repository or in its companion directory.
|
|
34
|
+
|
|
35
|
+
`aligndev code` reads the project's layout from `alignfirst config --json`. Session files go under its `_aligndev` location: `<ticket>/_aligndev/` or `_aligndev/`, below the project's `.plans/` unless the companion holds a separate tree. When some of the project's AlignFirst files exist in its companion, the normal permission modes make the companion writable for the coder (`--add-dir`), and a new session's prompt starts with the `alignfirst context` output.
|
|
36
|
+
|
|
37
|
+
A new protocol session needs a ticket. `--no-ticket` reserves the next side ticket through `alignfirst ticket --side` and passes it to the coder.
|
|
38
|
+
|
|
39
|
+
`--catchup` loads the ticket's history (through `alignfirst ticket --catchup`) before the protocol and message. Alone, it returns a short synthesis.
|
|
40
|
+
|
|
41
|
+
`--message-file <path>` reads the message from a UTF-8 file, or from stdin with `-`. The prompt reaches the coder through stdin.
|
|
42
|
+
|
|
43
|
+
`aligndev code status` reconciles and shows a run's durable status. It accepts a session file under `_aligndev/` or `<ticket>/_aligndev/` of the `_aligndev` location, or selects the newest run with `--ticket <id>`, `--no-ticket` or `--meta <key>`. If a recorded process is gone, it seals the session file as `status: failed`, `exitReason: terminated`. Linux records also store the process start time to detect pid reuse. Its `contextTokens` line reports what the run left in the coder's context window; a resumed session keeps growing across runs.
|
|
44
|
+
|
|
45
|
+
`aligndev code quota` shows the selected coding agent's account limits, consumed percentages, and reset times. It works outside a project.
|
|
46
|
+
|
|
47
|
+
### `aligndev project`
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
aligndev project list [--json] [--root <path>]
|
|
51
|
+
aligndev project doctor [--root <path>]
|
|
52
|
+
aligndev project status <path> [--json] [--root <path>]
|
|
53
|
+
aligndev project init [--root <path>] [--description <text>] [--port-range [<code>=]<first>-<last>]...
|
|
54
|
+
aligndev project free-ports --size <n> [--range <code>] [--json] [--root <path>]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A projects directory groups projects and optional nested projects directories. Its `.alignfirst-projects.json` marker holds an optional description and port ranges. A direct child that is a Git main worktree is a project; linked Git worktrees are listed as its workspaces. A child outside Git with a root `.alignfirst.json` is an inventory issue. `list --json` and `status` report each project's companion directory and the location of its AlignFirst files. Two projects sharing one companion directory is an inventory issue.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"description": "Every project is a direct child of ~/projects.",
|
|
62
|
+
"portRanges": [
|
|
63
|
+
{ "first": 28000, "last": 28599, "description": "Web projects, exposed through the gateway." },
|
|
64
|
+
{ "code": "local", "first": 29000, "last": 29199, "description": "Desktop apps, never exposed." }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`--root` defaults to `projectsRoot` in the config, then to the working directory. `doctor` is a read-only health gate: it succeeds only when discovery completes with no inventory issue.
|
|
70
|
+
|
|
71
|
+
An older marker carrying `"portRange": { ... }` is rejected; replace the key with `"portRanges": [{ ... }]`. Project configuration in `.alignfirst.json` keeps its singular `portRange` key.
|
|
72
|
+
|
|
73
|
+
### `aligndev guide`
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
aligndev guide [<topic>] [--root <path>]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Each topic renders the variant of the configured `platform`.
|
|
80
|
+
|
|
81
|
+
| Topic | Output | Platforms |
|
|
82
|
+
|-------|--------|-----------|
|
|
83
|
+
| (none) | The playbook dispatcher. | both |
|
|
84
|
+
| `working-session` | The work procedure: a thread under OpenClaw, the conversation for a coding agent. | both |
|
|
85
|
+
| `project-workspace-setup`, `consultation` | Runbooks. | both |
|
|
86
|
+
| `code` | The delegation guide. | both |
|
|
87
|
+
| `channel-handling` | The channel and DM procedure. | `openclaw` |
|
|
88
|
+
| `project-lifecycle` | The runbook to create, onboard or remove a project. | `openclaw` |
|
|
89
|
+
| `slack-message-tool`, `discord-message-tool` | Extended `message` references. | `openclaw` |
|
|
90
|
+
| `project` | The projects guide, followed by the directory sections when the root carries a marker. `--root` overrides `projectsRoot`. | `openclaw` |
|
|
91
|
+
|
|
92
|
+
Under `openclaw`, the playbook topics require `projectsRoot`.
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
`aligndev` reads one file, `~/.config/alignfirst/aligndev.config.json`. The path is fixed: no environment variable overrides it. `aligndev code` and `aligndev guide` require it, `--help` included. Without it, they fail with an error naming the path and the required keys. `aligndev project`, `aligndev --help` and `aligndev --version` run without it.
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"platform": "openclaw",
|
|
101
|
+
"projectsRoot": "~/projects",
|
|
102
|
+
"code": {
|
|
103
|
+
"agent": "claude",
|
|
104
|
+
"models": ["opus", "sonnet"],
|
|
105
|
+
"skipPermissions": false,
|
|
106
|
+
"unset": ["ANTHROPIC_API_KEY"]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A coding agent acting as the assistant needs only the required keys:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"platform": "codingAgent",
|
|
116
|
+
"code": { "agent": "codex" }
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- `platform` — required. Selects the variant of every `aligndev guide` topic: `openclaw` for an OpenClaw assistant, `codingAgent` for a coding agent acting as the assistant.
|
|
121
|
+
- `projectsRoot` — the default projects directory of `aligndev project` and `aligndev guide project`. Required by the `openclaw` playbook. `~/` expands to the home directory; a relative path resolves against the config file's directory.
|
|
122
|
+
- `code.agent` — required. The coder: `claude` or `codex`.
|
|
123
|
+
- `code.models` — replaces the selected agent's accepted models.
|
|
124
|
+
- `code.skipPermissions` — `true` selects each CLI's dangerous permission-bypass flag. Default: `false`.
|
|
125
|
+
- `code.unset` — environment variables stripped from the coder's environment. `aligndev code` always strips the assistant session's identity variables first (`CLAUDECODE`, `CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, …), whatever the agent.
|
|
126
|
+
|
|
127
|
+
Unknown keys are rejected. An unreadable file, invalid JSON or an invalid value fails every command that loads the config, with an error naming the file.
|
|
128
|
+
|
|
129
|
+
Companion directories are declared in `~/.config/alignfirst/companions.json`, which the `alignfirst` CLI reads for both tools. See [its README](https://github.com/paleo/alignfirst/tree/main/packages/alignfirst#companion-directories).
|
|
130
|
+
|
|
131
|
+
## Execution model
|
|
132
|
+
|
|
133
|
+
`aligndev code` runs the coder as a direct **foreground** child of its own process. It streams a live transcript to stdout and to a per-run session file, whose frontmatter status goes from `running` to `succeeded` or `failed`, and blocks until the coder exits. It never backgrounds or detaches itself.
|
|
134
|
+
|
|
135
|
+
Coding runs can be very long, so the caller always runs `aligndev code` as a background task and owns the backgrounding, as `aligndev guide code` prescribes:
|
|
136
|
+
|
|
137
|
+
- Under OpenClaw, the assistant invokes it through the `exec` tool with `background: true` and `timeoutSeconds: 0`, and chains `openclaw system event --mode now --session-key <key>` onto the command.
|
|
138
|
+
- A coding-agent assistant starts it with its own background-execution facility, with no time limit, and outside its sandbox. When the agent wakes the session as the command exits, as Claude Code does, the assistant handles the completion on that wake. Otherwise, the assistant checks its pending runs at the start of the next user turn.
|
|
139
|
+
|
|
140
|
+
The completion turn locates the session file with `aligndev code status` and reads the result. The session file is the durable result handoff: frontmatter `sessionId` and status, and the `---- Result ----` block.
|
|
141
|
+
|
|
142
|
+
If `aligndev code` is terminated, its signal handlers seal the session file (`status: failed`, `exitReason: terminated`), then send `SIGTERM` to the coder. After a short grace period, a `SIGKILL` guarantees no orphan is left behind. Only a `SIGKILL` of `aligndev` itself can leave a stale `running` status, which the next `status` call seals.
|
|
143
|
+
|
|
144
|
+
When the coding agent's session on the host is missing or expired, `aligndev code` detects the authentication failure in its stream, seals the session file with `exitReason: auth_required`, and exits `2` with a one-line stderr message.
|
|
145
|
+
|
|
146
|
+
## Coding agents
|
|
147
|
+
|
|
148
|
+
Install the selected CLI and authenticate it on the host: run `claude`, then `/login`, for Claude Code; run `codex login` for Codex.
|
|
149
|
+
|
|
150
|
+
Normal runs use Claude's `--permission-mode auto` or Codex's `--sandbox workspace-write`.
|
|
151
|
+
|
|
152
|
+
Claude's default model list is `fable`, `opus`, `sonnet`, `haiku`. Codex's is `astra`, `sol`, `terra`, `luna`; `aligndev code` resolves a selected Codex alias against `codex debug models --bundled`. Set `code.models` to narrow the list or to advertise an explicit Codex slug such as `gpt-5.6-terra`.
|
|
153
|
+
|
|
154
|
+
Session files record `agent`. A session resumes only with the same selected agent. Agentless legacy sessions stay readable but require a new session.
|
|
155
|
+
|
|
156
|
+
## The `aligndev` skill
|
|
157
|
+
|
|
158
|
+
The `aligndev` agent skill makes a Claude Code or Codex session the assistant of the project it runs in. Invoked as `/aligndev` in Claude Code, or `$aligndev` in Codex, it loads the playbook through `npx -y aligndev guide`, which needs `platform: "codingAgent"` in the config. Install it:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
npx skills add https://github.com/paleo/alignfirst --global --skill aligndev
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The setup guide's [coding-agent assistant reference](https://github.com/paleo/alignfirst/blob/main/skills/alignfirst-setup-guide/references/coding-agent-assistant.md) covers the project, the configuration and the sandbox.
|
|
165
|
+
|
|
166
|
+
## Port claims
|
|
167
|
+
|
|
168
|
+
Run `aligndev project free-ports --size <n>` with the block size required by the project's workspace scheme: `perWorkspace × maxWorkspaces`. A marker entry without a code is the default range. Pass `--range <code>` to select a coded range. The setup guide writes the returned block as `portRange` in the project's `.alignfirst.json`.
|
package/bin/aligndev.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface AlignfirstResult {
|
|
2
|
+
status: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runAlignfirst(command: string[], args: string[], cwd: string, env?: NodeJS.ProcessEnv): AlignfirstResult;
|
|
7
|
+
export declare function reserveSideTicket(command: string[], cwd: string, env: NodeJS.ProcessEnv): string;
|
|
8
|
+
export declare function openTicket(command: string[], cwd: string, ticket: string, env: NodeJS.ProcessEnv): void;
|
|
9
|
+
export declare function loadCatchup(command: string[], cwd: string, ticket: string, env: NodeJS.ProcessEnv): string;
|
|
10
|
+
export declare function loadContext(command: string[], cwd: string, env: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export function runAlignfirst(command, args, cwd, env = process.env) {
|
|
3
|
+
const result = spawnSync(command[0], [...command.slice(1), ...args], {
|
|
4
|
+
cwd,
|
|
5
|
+
env,
|
|
6
|
+
encoding: "utf8",
|
|
7
|
+
});
|
|
8
|
+
if (result.error && isErrnoException(result.error) && result.error.code === "ENOENT") {
|
|
9
|
+
throw new Error("alignfirst is not installed. Install it globally (`npm install -g alignfirst`), or run " +
|
|
10
|
+
"aligndev through npx (`npx -y aligndev`).");
|
|
11
|
+
}
|
|
12
|
+
if (result.error)
|
|
13
|
+
throw result.error;
|
|
14
|
+
return {
|
|
15
|
+
status: result.status ?? 1,
|
|
16
|
+
stdout: result.stdout,
|
|
17
|
+
stderr: result.stderr,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function reserveSideTicket(command, cwd, env) {
|
|
21
|
+
const result = runAlignfirst(command, ["ticket", "--side", "--json"], cwd, env);
|
|
22
|
+
if (result.status !== 0) {
|
|
23
|
+
throw new Error(result.stderr.trim() || "alignfirst ticket --side failed");
|
|
24
|
+
}
|
|
25
|
+
const report = JSON.parse(result.stdout);
|
|
26
|
+
if (!isRecord(report) || typeof report.TICKET_ID !== "string") {
|
|
27
|
+
throw new Error("alignfirst ticket --side returned an invalid JSON report");
|
|
28
|
+
}
|
|
29
|
+
return report.TICKET_ID;
|
|
30
|
+
}
|
|
31
|
+
// Creates or restores the ticket's directories, as `alignfirst ticket <id>` does for a developer.
|
|
32
|
+
export function openTicket(command, cwd, ticket, env) {
|
|
33
|
+
const result = runAlignfirst(command, ["ticket", ticket, "--json"], cwd, env);
|
|
34
|
+
if (result.status !== 0)
|
|
35
|
+
throw new Error(result.stderr.trim() || "alignfirst ticket failed");
|
|
36
|
+
}
|
|
37
|
+
export function loadCatchup(command, cwd, ticket, env) {
|
|
38
|
+
const result = runAlignfirst(command, ["ticket", ticket, "--catchup"], cwd, env);
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
throw new Error(result.stderr.trim() || "alignfirst ticket --catchup failed");
|
|
41
|
+
}
|
|
42
|
+
return result.stdout;
|
|
43
|
+
}
|
|
44
|
+
export function loadContext(command, cwd, env) {
|
|
45
|
+
const result = runAlignfirst(command, ["context"], cwd, env);
|
|
46
|
+
if (result.status !== 0) {
|
|
47
|
+
throw new Error(result.stderr.trim() || "alignfirst context failed");
|
|
48
|
+
}
|
|
49
|
+
return result.stdout;
|
|
50
|
+
}
|
|
51
|
+
function isRecord(value) {
|
|
52
|
+
return typeof value === "object" && value !== null;
|
|
53
|
+
}
|
|
54
|
+
function isErrnoException(error) {
|
|
55
|
+
return "code" in error;
|
|
56
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ExecutableModelResolver } from "./code/models.js";
|
|
2
|
+
import { type QuotaReader } from "./code/quota.js";
|
|
3
|
+
import type { Output } from "./output.js";
|
|
4
|
+
export interface MainOptions {
|
|
5
|
+
argv?: string[];
|
|
6
|
+
stdout?: Output;
|
|
7
|
+
stderr?: Output;
|
|
8
|
+
cwd?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
home?: string;
|
|
11
|
+
alignfirstCommand?: string[];
|
|
12
|
+
modelResolver?: ExecutableModelResolver;
|
|
13
|
+
quotaReader?: QuotaReader;
|
|
14
|
+
}
|
|
15
|
+
export declare function main(options?: MainOptions): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { runCode } from "./code/code-cli.js";
|
|
4
|
+
import { CODING_AGENTS } from "./code/coding-agent.js";
|
|
5
|
+
import { resolveExecutableModel } from "./code/models.js";
|
|
6
|
+
import { readQuota } from "./code/quota.js";
|
|
7
|
+
import { resolveCommandForms } from "./command-form.js";
|
|
8
|
+
import { loadConfig, missingConfigMessage, PLATFORMS } from "./config.js";
|
|
9
|
+
import { errorMessage } from "./errors.js";
|
|
10
|
+
import { runGuide } from "./guide/guide-cli.js";
|
|
11
|
+
import { runProject } from "./project/project-cli.js";
|
|
12
|
+
export async function main(options) {
|
|
13
|
+
const ctx = resolveContext(options);
|
|
14
|
+
const [command, ...tokens] = (options?.argv ?? process.argv).slice(2);
|
|
15
|
+
if (command === "--help" || command === "-h") {
|
|
16
|
+
ctx.stdout.write(renderHelp(ctx.forms.aligndev));
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
if (command === "--version" || command === "-v") {
|
|
20
|
+
ctx.stdout.write(`${readPackageVersion()}\n`);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (command !== "code" && command !== "project" && command !== "guide") {
|
|
24
|
+
const error = command === undefined ? "Error: no command given." : `Error: unknown command "${command}".`;
|
|
25
|
+
ctx.stderr.write(`${error}\n\n${renderHelp(ctx.forms.aligndev)}`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
let config;
|
|
29
|
+
try {
|
|
30
|
+
config = loadConfig(ctx.home);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
ctx.stderr.write(`${errorMessage(error)}\n`);
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
if (command === "project")
|
|
37
|
+
return runProject(tokens, config, ctx);
|
|
38
|
+
if (config === undefined) {
|
|
39
|
+
ctx.stderr.write(`${missingConfigMessage(ctx.home)}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (command === "code")
|
|
43
|
+
return runCode(tokens, config, ctx);
|
|
44
|
+
return runGuide(tokens, config, ctx);
|
|
45
|
+
}
|
|
46
|
+
function renderHelp(aligndev) {
|
|
47
|
+
const usage = renderUsageRows([
|
|
48
|
+
[`${aligndev} code <command> [<options>]`, "Run a coding agent through AlignFirst protocols."],
|
|
49
|
+
[
|
|
50
|
+
`${aligndev} project <command> [<options>]`,
|
|
51
|
+
"List projects, check their inventory, claim port ranges.",
|
|
52
|
+
],
|
|
53
|
+
[`${aligndev} guide [<topic>]`, "Print the assistant's playbook and guides."],
|
|
54
|
+
[`${aligndev} -h, --help`],
|
|
55
|
+
[`${aligndev} -v, --version`],
|
|
56
|
+
]);
|
|
57
|
+
return `aligndev — the AlignFirst Dev Kit CLI.
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
${usage}
|
|
61
|
+
|
|
62
|
+
Run \`${aligndev} <command> --help\` for the usage of a command.
|
|
63
|
+
|
|
64
|
+
Config: ~/.config/alignfirst/aligndev.config.json, with "platform" (${PLATFORMS.join(" or ")}) and
|
|
65
|
+
"code.agent" (${CODING_AGENTS.join(" or ")}). \`${aligndev} code\` and \`${aligndev} guide\` require it.
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
// Each row is a command, optionally followed by its description in an aligned column.
|
|
69
|
+
function renderUsageRows(rows) {
|
|
70
|
+
const width = Math.max(...rows.map(([command]) => command.length)) + 3;
|
|
71
|
+
return rows
|
|
72
|
+
.map(([command, description]) => description === undefined ? ` ${command}` : ` ${command.padEnd(width)}${description}`)
|
|
73
|
+
.join("\n");
|
|
74
|
+
}
|
|
75
|
+
function resolveContext(options) {
|
|
76
|
+
const env = options?.env ?? process.env;
|
|
77
|
+
const forms = resolveCommandForms(env);
|
|
78
|
+
return {
|
|
79
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
80
|
+
env,
|
|
81
|
+
home: options?.home ?? env.HOME ?? env.USERPROFILE ?? homedir(),
|
|
82
|
+
stdout: options?.stdout ?? process.stdout,
|
|
83
|
+
stderr: options?.stderr ?? process.stderr,
|
|
84
|
+
forms,
|
|
85
|
+
alignfirstCommand: options?.alignfirstCommand ?? forms.alignfirst.split(" "),
|
|
86
|
+
modelResolver: options?.modelResolver ?? resolveExecutableModel,
|
|
87
|
+
quotaReader: options?.quotaReader ?? readQuota,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function readPackageVersion() {
|
|
91
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
92
|
+
if (pkg.version === undefined)
|
|
93
|
+
throw new Error("aligndev: package.json is missing 'version'");
|
|
94
|
+
return pkg.version;
|
|
95
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentAdapter, AgentProtocolState, RunConfig } from "./run-agent.js";
|
|
2
|
+
export declare function createClaudeAdapter(): AgentAdapter;
|
|
3
|
+
export declare function buildClaudeArgs(config: RunConfig): string[];
|
|
4
|
+
export declare function createClaudeState(): AgentProtocolState;
|
|
5
|
+
export declare function interpretClaudeLine(line: string, state: AgentProtocolState): string | undefined;
|
|
6
|
+
export declare function assessClaudeState(state: AgentProtocolState): {
|
|
7
|
+
succeeded: boolean;
|
|
8
|
+
sessionId: string | undefined;
|
|
9
|
+
result: string | undefined;
|
|
10
|
+
error: string | undefined;
|
|
11
|
+
authEvidence: boolean;
|
|
12
|
+
contextTokens: number | undefined;
|
|
13
|
+
contextCompacted: boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { isCompacted } from "./run-agent.js";
|
|
2
|
+
export function createClaudeAdapter() {
|
|
3
|
+
return {
|
|
4
|
+
executable: "claude",
|
|
5
|
+
buildArgs: buildClaudeArgs,
|
|
6
|
+
createState: createClaudeState,
|
|
7
|
+
interpretLine: interpretClaudeLine,
|
|
8
|
+
assess: assessClaudeState,
|
|
9
|
+
isAuthenticationError: () => false,
|
|
10
|
+
authenticationMessage: claudeAuthenticationMessage,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function buildClaudeArgs(config) {
|
|
14
|
+
const args = ["-p", "--output-format", "stream-json", "--verbose"];
|
|
15
|
+
if (config.skipPermissions) {
|
|
16
|
+
args.push("--dangerously-skip-permissions");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
args.push("--permission-mode", "auto");
|
|
20
|
+
}
|
|
21
|
+
// `--add-dir` is variadic: only options may follow it. The prompt goes through stdin.
|
|
22
|
+
for (const dir of config.additionalDirectories)
|
|
23
|
+
args.push("--add-dir", dir);
|
|
24
|
+
if (config.resume !== undefined)
|
|
25
|
+
args.push("--resume", config.resume);
|
|
26
|
+
if (config.executableModel !== undefined)
|
|
27
|
+
args.push("--model", config.executableModel);
|
|
28
|
+
return args;
|
|
29
|
+
}
|
|
30
|
+
export function createClaudeState() {
|
|
31
|
+
return {
|
|
32
|
+
protocolComplete: false,
|
|
33
|
+
protocolFailed: false,
|
|
34
|
+
authEvidence: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function interpretClaudeLine(line, state) {
|
|
38
|
+
const event = parseEventLine(line);
|
|
39
|
+
if (!isRecord(event))
|
|
40
|
+
return;
|
|
41
|
+
captureSessionId(event, state);
|
|
42
|
+
if (event.error === "authentication_failed")
|
|
43
|
+
state.authEvidence = true;
|
|
44
|
+
switch (event.type) {
|
|
45
|
+
case "system":
|
|
46
|
+
return event.subtype === "init" ? `[init] session ${asString(event.session_id)}` : undefined;
|
|
47
|
+
case "assistant":
|
|
48
|
+
captureContextTokens(event, state);
|
|
49
|
+
return renderMessageContent(event);
|
|
50
|
+
case "user":
|
|
51
|
+
return renderMessageContent(event);
|
|
52
|
+
case "result":
|
|
53
|
+
state.protocolComplete = true;
|
|
54
|
+
state.protocolFailed = event.is_error === true;
|
|
55
|
+
state.result = asString(event.result);
|
|
56
|
+
if (state.protocolFailed)
|
|
57
|
+
state.failure = state.result;
|
|
58
|
+
return;
|
|
59
|
+
case "unparsed":
|
|
60
|
+
return asString(event.raw);
|
|
61
|
+
default:
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function assessClaudeState(state) {
|
|
66
|
+
return {
|
|
67
|
+
succeeded: state.protocolComplete && !state.protocolFailed && state.result !== undefined,
|
|
68
|
+
sessionId: state.sessionId,
|
|
69
|
+
result: state.result,
|
|
70
|
+
error: state.failure,
|
|
71
|
+
authEvidence: state.authEvidence,
|
|
72
|
+
contextTokens: state.contextTokens,
|
|
73
|
+
contextCompacted: isCompacted(state),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// What the newest assistant response holds in the context window. Claude reports cache reads and
|
|
77
|
+
// writes beside `input_tokens`, so the occupancy is their sum plus the response itself. A subagent
|
|
78
|
+
// message carries `parent_tool_use_id` and measures its own context, not the main conversation's.
|
|
79
|
+
function captureContextTokens(event, state) {
|
|
80
|
+
if (event.parent_tool_use_id != null)
|
|
81
|
+
return;
|
|
82
|
+
const message = event.message;
|
|
83
|
+
if (!isRecord(message) || !isRecord(message.usage))
|
|
84
|
+
return;
|
|
85
|
+
const usage = message.usage;
|
|
86
|
+
const total = asCount(usage.input_tokens) +
|
|
87
|
+
asCount(usage.cache_creation_input_tokens) +
|
|
88
|
+
asCount(usage.cache_read_input_tokens) +
|
|
89
|
+
asCount(usage.output_tokens);
|
|
90
|
+
if (total === 0)
|
|
91
|
+
return;
|
|
92
|
+
state.contextTokens = total;
|
|
93
|
+
state.peakContextTokens = Math.max(state.peakContextTokens ?? 0, total);
|
|
94
|
+
}
|
|
95
|
+
function parseEventLine(line) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(line);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { type: "unparsed", raw: line };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function captureSessionId(event, state) {
|
|
104
|
+
const id = asString(event.session_id);
|
|
105
|
+
if (id !== undefined && id !== "")
|
|
106
|
+
state.sessionId = id;
|
|
107
|
+
}
|
|
108
|
+
function renderMessageContent(event) {
|
|
109
|
+
const message = event.message;
|
|
110
|
+
if (!isRecord(message) || !Array.isArray(message.content))
|
|
111
|
+
return;
|
|
112
|
+
const parts = [];
|
|
113
|
+
for (const block of message.content) {
|
|
114
|
+
const rendered = renderBlock(block);
|
|
115
|
+
if (rendered !== undefined && rendered !== "")
|
|
116
|
+
parts.push(rendered);
|
|
117
|
+
}
|
|
118
|
+
return parts.length > 0 ? parts.join("\n") : undefined;
|
|
119
|
+
}
|
|
120
|
+
function renderBlock(block) {
|
|
121
|
+
if (!isRecord(block))
|
|
122
|
+
return;
|
|
123
|
+
switch (block.type) {
|
|
124
|
+
case "text":
|
|
125
|
+
return asString(block.text);
|
|
126
|
+
case "tool_use":
|
|
127
|
+
return `[tool: ${asString(block.name) ?? "?"}] ${compactJson(block.input)}`;
|
|
128
|
+
case "tool_result":
|
|
129
|
+
return `[tool result] ${truncate(renderToolResult(block.content), 500)}`;
|
|
130
|
+
default:
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function renderToolResult(content) {
|
|
135
|
+
if (typeof content === "string")
|
|
136
|
+
return content;
|
|
137
|
+
if (Array.isArray(content)) {
|
|
138
|
+
return content.map((block) => (isRecord(block) ? (asString(block.text) ?? "") : "")).join("");
|
|
139
|
+
}
|
|
140
|
+
return compactJson(content);
|
|
141
|
+
}
|
|
142
|
+
function claudeAuthenticationMessage(detail) {
|
|
143
|
+
const base = "Coding agent not authenticated (authentication_failed): the host session is missing, " +
|
|
144
|
+
"expired, or rejected. An administrator must re-login on the host (run `claude`, then " +
|
|
145
|
+
"`/login`) before `aligndev code` can run again.";
|
|
146
|
+
const reason = detail?.trim();
|
|
147
|
+
return reason === undefined || reason === "" ? base : `${base}\n\n${reason}`;
|
|
148
|
+
}
|
|
149
|
+
function isRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null;
|
|
151
|
+
}
|
|
152
|
+
function asString(value) {
|
|
153
|
+
return typeof value === "string" ? value : undefined;
|
|
154
|
+
}
|
|
155
|
+
function asCount(value) {
|
|
156
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
157
|
+
}
|
|
158
|
+
function compactJson(value) {
|
|
159
|
+
try {
|
|
160
|
+
return truncate(JSON.stringify(value) ?? "", 500);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function truncate(text, max) {
|
|
167
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
168
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CommandForms } from "../command-form.js";
|
|
2
|
+
import type { AligndevConfig, CodeConfig } from "../config.js";
|
|
3
|
+
import type { Output } from "../output.js";
|
|
4
|
+
import { type ProjectReport } from "../project/layout.js";
|
|
5
|
+
import { type CodingAgent } from "./coding-agent.js";
|
|
6
|
+
import { type ExecutableModelResolver } from "./models.js";
|
|
7
|
+
import type { QuotaReader } from "./quota.js";
|
|
8
|
+
import { type RunConfig } from "./run-agent.js";
|
|
9
|
+
import { type SessionRecord } from "./session-file.js";
|
|
10
|
+
export interface CodeContext {
|
|
11
|
+
cwd: string;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
stdout: Output;
|
|
14
|
+
stderr: Output;
|
|
15
|
+
forms: CommandForms;
|
|
16
|
+
alignfirstCommand: string[];
|
|
17
|
+
modelResolver: ExecutableModelResolver;
|
|
18
|
+
quotaReader: QuotaReader;
|
|
19
|
+
}
|
|
20
|
+
export type CodeCommand = {
|
|
21
|
+
kind: "help";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "status";
|
|
24
|
+
target: StatusTarget;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "quota";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "session";
|
|
29
|
+
args: SessionArgs;
|
|
30
|
+
};
|
|
31
|
+
export type StatusTarget = {
|
|
32
|
+
kind: "file";
|
|
33
|
+
sessionFile: string;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "ticket";
|
|
36
|
+
ticket: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "noTicket";
|
|
39
|
+
} | {
|
|
40
|
+
kind: "meta";
|
|
41
|
+
meta: string;
|
|
42
|
+
};
|
|
43
|
+
export interface SessionArgs {
|
|
44
|
+
resume?: string;
|
|
45
|
+
ticket?: string;
|
|
46
|
+
noTicket: boolean;
|
|
47
|
+
protocol?: string;
|
|
48
|
+
catchup?: boolean;
|
|
49
|
+
messageFile?: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
meta?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function runCode(tokens: string[], config: AligndevConfig, ctx: CodeContext): Promise<number>;
|
|
55
|
+
export declare function parseCodeArgs(tokens: string[], aligndev: string): CodeCommand;
|
|
56
|
+
export declare function validateSessionArgs(args: SessionArgs, models: readonly string[]): string | undefined;
|
|
57
|
+
export declare function checkLaunchGuards(args: SessionArgs, agent: CodingAgent, realCwd: string, records: SessionRecord[]): string | undefined;
|
|
58
|
+
export declare function resolveTicket(args: SessionArgs, records: SessionRecord[]): string | undefined;
|
|
59
|
+
export interface RunInput {
|
|
60
|
+
args: SessionArgs;
|
|
61
|
+
code: CodeConfig;
|
|
62
|
+
report: ProjectReport;
|
|
63
|
+
ticket: string | undefined;
|
|
64
|
+
cwd: string;
|
|
65
|
+
sessionFilePath: string;
|
|
66
|
+
env: NodeJS.ProcessEnv;
|
|
67
|
+
executableModel: string | undefined;
|
|
68
|
+
catchupContent?: string;
|
|
69
|
+
contextContent?: string;
|
|
70
|
+
alignfirst: string;
|
|
71
|
+
}
|
|
72
|
+
export declare function buildRunConfig(input: RunInput): RunConfig;
|