@glrs-dev/cli 0.3.1 → 1.0.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/CHANGELOG.md +10 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +18 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +19 -9
- package/dist/vendor/harness-opencode/dist/chunk-57EOY72Y.js +174 -0
- package/dist/vendor/harness-opencode/dist/chunk-5TAMY7P6.js +67 -0
- package/dist/vendor/harness-opencode/dist/chunk-BKTFWXLG.js +204 -0
- package/dist/vendor/harness-opencode/dist/chunk-KB7M7JXU.js +145 -0
- package/dist/vendor/harness-opencode/dist/chunk-RNRCXQ65.js +56 -0
- package/dist/vendor/harness-opencode/dist/cli.js +955 -1453
- package/dist/vendor/harness-opencode/dist/index.js +1 -1
- package/dist/vendor/harness-opencode/dist/paths-LT3QQKCF.js +18 -0
- package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.d.ts +1 -0
- package/dist/vendor/harness-opencode/dist/pilot/mcp/status-server.js +228 -0
- package/dist/vendor/harness-opencode/dist/pilot-config-7LJZ23YK.js +55 -0
- package/dist/vendor/harness-opencode/dist/runs-QWPL3TKV.js +18 -0
- package/dist/vendor/harness-opencode/dist/safety-gate-WM3EWOCY.js +10 -0
- package/dist/vendor/harness-opencode/dist/setup-hook-FHTXMAQL.js +88 -0
- package/dist/vendor/harness-opencode/dist/skills/adr/SKILL.md +328 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/SKILL.md +40 -13
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/decomposition.md +27 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/self-review.md +1 -1
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/touches-scope.md +34 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/verify-design.md +78 -14
- package/dist/vendor/harness-opencode/dist/tasks-KJ3WN2KY.js +32 -0
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +1 -1
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/setup-authoring.md +0 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @glrs-dev/cli
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#27](https://github.com/iceglober/glrs/pull/27) [`cf74f2d`](https://github.com/iceglober/glrs/commit/cf74f2dca60ee099a92a500d90de1c1886b6aed0) Thanks [@iceglober](https://github.com/iceglober)! - chore(changesets): move @glrs-dev/cli and @glrs-dev/harness-plugin-opencode from `linked` to `fixed`
|
|
8
|
+
|
|
9
|
+
The `linked` group synchronizes versions only among packages that are ALREADY being bumped — it does not force a package into a release. A changeset that named only the harness (as most of our changesets do) would ship a new harness on npm without republishing the CLI, even though the CLI vendors the harness `dist/` at build time (`packages/cli/scripts/vendor-harness.ts`). End users running `glrs oc ...` would keep getting the old vendored harness until somebody remembered to write a no-op CLI changeset.
|
|
10
|
+
|
|
11
|
+
Moving the pair to `fixed` guarantees any harness publish drags the CLI along at a matching version, so a fresh CLI tarball always re-vendors the latest harness `dist/`. The trade-off — CLI-only changesets now also force a no-op harness republish — is cheap because CLI-only changes are rare in this repo.
|
|
12
|
+
|
|
3
13
|
## 0.3.1
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
|
@@ -80,7 +80,7 @@ If `task.prompt` says "add lodash to handle deep merging", install it. If the ta
|
|
|
80
80
|
|
|
81
81
|
If a verify failure clearly points to an environmental issue — `Cannot find module 'X'` where `X` is a workspace/monorepo dep, `node_modules` absent despite a lockfile committed to the repo, a stale build artifact a typecheck depends on — you ARE expected to run the obvious install command BEFORE giving up with STOP.
|
|
82
82
|
|
|
83
|
-
Recognise these canonical bootstrap commands: `pnpm install`, `bun install`, `npm install`, `npm ci`, `cargo fetch`, `cargo build`.
|
|
83
|
+
Recognise these canonical bootstrap commands: `pnpm install`, `bun install`, `npm install`, `npm ci`, `cargo fetch`, `cargo build`.
|
|
84
84
|
|
|
85
85
|
The plugin deny list does not block any of these; they are not task-level dependency additions and they do not require lockfile edits.
|
|
86
86
|
|
|
@@ -111,7 +111,22 @@ If the fix prompt names `touchesViolators`: revert your edits to those files. Us
|
|
|
111
111
|
- Plan. The plan is `pilot.yaml`. Each task in it was already designed by the pilot-planner agent. You are not a co-author.
|
|
112
112
|
- Refactor unrelated code. The task names a scope; respect it. If you see a glaring issue elsewhere, ignore it — that's a separate task for the human.
|
|
113
113
|
- Add observability/logging beyond what the task asks for. If the task didn't say "add structured logs", don't add structured logs.
|
|
114
|
-
- Run the verify commands yourself. The worker runs them after you stop. Running them yourself wastes turns and can leave residue (test artifacts, cached state) that messes up the worker's run.
|
|
115
114
|
- Apologize, hedge, or narrate. Each turn is a billable opencode session call; chat preamble buys you nothing.
|
|
115
|
+
- **Write TODO, FIXME, HACK, or XXX comments.** Many repos have pre-commit hooks that reject these annotations. The worker commits your work automatically after verify passes; if the commit is blocked by a hook, the task fails. If you need to note future work, put it in the task's output summary, not in a code comment.
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
# Self-verification — run the tests BEFORE you stop
|
|
118
|
+
|
|
119
|
+
**You SHOULD run the task's verify commands yourself during your work session.** The worker runs them formally after you stop, but you should iterate locally first:
|
|
120
|
+
|
|
121
|
+
1. Write the code.
|
|
122
|
+
2. Run the verify command(s) listed in the task's `verify:` field.
|
|
123
|
+
3. If they fail, fix the code and re-run. Iterate until they pass.
|
|
124
|
+
4. THEN stop.
|
|
125
|
+
|
|
126
|
+
This is faster and cheaper than the worker's retry loop (which requires a full session round-trip per attempt). The worker's formal verify is a gate, not your development loop — arrive at the gate already passing.
|
|
127
|
+
|
|
128
|
+
**How to find the verify commands:** They're in the task kickoff prompt under "Verify commands". Run them exactly as written via bash. They execute in the repo root (cwd).
|
|
129
|
+
|
|
130
|
+
**Exception:** If a verify command requires infrastructure you can't reach (e.g., a running server on a specific port), note that in your output and stop. The worker will handle it.
|
|
131
|
+
|
|
132
|
+
You're a focused, fast, pessimistic implementer. Make the change. Verify it passes. Stop.
|
|
@@ -45,13 +45,13 @@ Use Serena and grep to map out:
|
|
|
45
45
|
- Existing tests that already cover related code (the verify commands will likely be variations of those).
|
|
46
46
|
- Existing patterns the change should match.
|
|
47
47
|
- Any module boundaries that suggest natural task splits.
|
|
48
|
-
- **Tooling footprint** — lockfiles, docker-compose services, migration tooling, UI/API/DB test frameworks.
|
|
48
|
+
- **Tooling footprint** — lockfiles, docker-compose services, migration tooling, UI/API/DB test frameworks. Understanding these informs your per-surface verify patterns in Section 3.
|
|
49
49
|
|
|
50
50
|
Be thorough here. A planner who shipped a sloppy plan because they only skimmed the codebase wastes hours of pilot-builder time chasing bad scope.
|
|
51
51
|
|
|
52
52
|
## 3. Apply the planning methodology
|
|
53
53
|
|
|
54
|
-
The `pilot-planning` skill carries the
|
|
54
|
+
The `pilot-planning` skill carries the nine rules. Apply them:
|
|
55
55
|
|
|
56
56
|
1. First-principles task framing.
|
|
57
57
|
2. Decomposition into right-sized tasks.
|
|
@@ -61,8 +61,7 @@ The `pilot-planning` skill carries the ten rules. Apply them:
|
|
|
61
61
|
6. Optional milestone grouping.
|
|
62
62
|
7. Self-review.
|
|
63
63
|
8. Per-task `context:` population (rationale, code pointers, acceptance shorthand).
|
|
64
|
-
9. **
|
|
65
|
-
10. **QA-expectations establishment** — detect per-surface test frameworks and propose concrete verify patterns:
|
|
64
|
+
9. **QA-expectations establishment** — detect per-surface test frameworks and propose concrete verify patterns:
|
|
66
65
|
- **UI**: Playwright, Cypress, or Vitest browser mode for visual/interaction assertions
|
|
67
66
|
- **API**: curl against local endpoints or OpenAPI-based contract tests
|
|
68
67
|
- **DB**: Postgres readiness checks and migration verification (prisma migrate, drizzle-kit push)
|
|
@@ -70,7 +69,9 @@ The `pilot-planning` skill carries the ten rules. Apply them:
|
|
|
70
69
|
- **Browser-based component**: Storybook or Chromatic visual tests
|
|
71
70
|
- **CLI**: bin/ smoke tests or `--help` verification
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
Rule 9 typically involves ONE bundled `question` tool call to the user for QA verify patterns (respecting "talk to the user — once" guidance).
|
|
73
|
+
|
|
74
|
+
Note: The `setup:` field was removed in the cwd-mode rollback. Plans assume the user's dev stack is already running (install, compose, migrate, seed) before `pilot build` is invoked. Remind the user of this at hand-off.
|
|
74
75
|
|
|
75
76
|
## 4. Write the YAML
|
|
76
77
|
|
|
@@ -80,10 +81,6 @@ Required schema (see `src/pilot/plan/schema.ts` for the canonical Zod definition
|
|
|
80
81
|
|
|
81
82
|
```yaml
|
|
82
83
|
name: <human-readable plan name>
|
|
83
|
-
setup: # optional — run once per worktree before any task
|
|
84
|
-
- pnpm install --frozen-lockfile
|
|
85
|
-
- docker compose up -d postgres
|
|
86
|
-
- pnpm prisma migrate dev
|
|
87
84
|
defaults: # optional, override per-task as needed
|
|
88
85
|
agent: pilot-builder # default
|
|
89
86
|
model: anthropic/claude-sonnet-4-6
|
|
@@ -114,6 +111,17 @@ tasks:
|
|
|
114
111
|
touches:
|
|
115
112
|
- src/api/**
|
|
116
113
|
- test/api/**
|
|
114
|
+
tolerate: # optional — files that may appear in
|
|
115
|
+
# the diff but aren't part of the task's
|
|
116
|
+
# scope (project-specific codegen,
|
|
117
|
+
# framework side-effects beyond the
|
|
118
|
+
# built-in defaults like next-env.d.ts).
|
|
119
|
+
# Common entries: prisma/client/**,
|
|
120
|
+
# graphql/generated/**, schema.graphql.
|
|
121
|
+
# Built-in defaults already cover
|
|
122
|
+
# next-env.d.ts, .next/types/**,
|
|
123
|
+
# *.tsbuildinfo, __snapshots__/**.
|
|
124
|
+
- prisma/client/**
|
|
117
125
|
verify:
|
|
118
126
|
- bun test test/api
|
|
119
127
|
depends_on: [ ] # other task ids
|
|
@@ -154,6 +162,8 @@ Don't elaborate. Don't summarize the plan in chat. The user can read it.
|
|
|
154
162
|
|
|
155
163
|
- **Asking the human to clarify mid-build.** Don't write tasks whose prompts contain things like "ask the user about X". Pilot is unattended. If you don't know X, either ASK NOW (during the planning session) or design the task to discover X via reading code.
|
|
156
164
|
|
|
165
|
+
- **YAML quoting errors in titles/prompts.** If a string contains double quotes, wrap it in single quotes: `title: '"Test rule set" UI + hook'`. If it contains single quotes, use double quotes with escaped inner quotes: `title: "it's a \"test\""`. NEVER write `title: "word" more words` — YAML closes the scalar at the second `"`. Run `pilot validate` after saving; it catches these.
|
|
166
|
+
|
|
157
167
|
# What "done" looks like
|
|
158
168
|
|
|
159
169
|
A plan that:
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/pilot/state/tasks.ts
|
|
2
|
+
function upsertFromPlan(db, runId, plan) {
|
|
3
|
+
const stmt = db.prepare(
|
|
4
|
+
`INSERT OR IGNORE INTO tasks (run_id, task_id, status) VALUES (?, ?, 'pending')`
|
|
5
|
+
);
|
|
6
|
+
const tx = db.transaction(() => {
|
|
7
|
+
for (const t of plan.tasks) {
|
|
8
|
+
stmt.run(runId, t.id);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
tx();
|
|
12
|
+
}
|
|
13
|
+
function markReady(db, runId, taskId) {
|
|
14
|
+
requireStatus(db, runId, taskId, ["pending"], "ready");
|
|
15
|
+
db.run(
|
|
16
|
+
"UPDATE tasks SET status='ready' WHERE run_id=? AND task_id=?",
|
|
17
|
+
[runId, taskId]
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function markRunning(db, args) {
|
|
21
|
+
requireStatus(db, args.runId, args.taskId, ["ready"], "running");
|
|
22
|
+
const now = args.now ?? Date.now();
|
|
23
|
+
db.run(
|
|
24
|
+
`UPDATE tasks
|
|
25
|
+
SET status='running',
|
|
26
|
+
attempts = attempts + 1,
|
|
27
|
+
session_id = ?,
|
|
28
|
+
branch = ?,
|
|
29
|
+
worktree_path = ?,
|
|
30
|
+
started_at = COALESCE(started_at, ?)
|
|
31
|
+
WHERE run_id=? AND task_id=?`,
|
|
32
|
+
[args.sessionId, args.branch, args.worktreePath, now, args.runId, args.taskId]
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function markSucceeded(db, runId, taskId, now = Date.now()) {
|
|
36
|
+
requireStatus(db, runId, taskId, ["running"], "succeeded");
|
|
37
|
+
db.run(
|
|
38
|
+
`UPDATE tasks
|
|
39
|
+
SET status='succeeded', finished_at=?, last_error=NULL
|
|
40
|
+
WHERE run_id=? AND task_id=?`,
|
|
41
|
+
[now, runId, taskId]
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function markFailed(db, runId, taskId, reason, now = Date.now()) {
|
|
45
|
+
requireStatus(db, runId, taskId, ["running", "ready"], "failed");
|
|
46
|
+
db.run(
|
|
47
|
+
`UPDATE tasks
|
|
48
|
+
SET status='failed', finished_at=?, last_error=?
|
|
49
|
+
WHERE run_id=? AND task_id=?`,
|
|
50
|
+
[now, reason, runId, taskId]
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function markBlocked(db, runId, taskId, reason) {
|
|
54
|
+
requireStatus(db, runId, taskId, ["pending", "ready"], "blocked");
|
|
55
|
+
db.run(
|
|
56
|
+
`UPDATE tasks
|
|
57
|
+
SET status='blocked', last_error=?
|
|
58
|
+
WHERE run_id=? AND task_id=?`,
|
|
59
|
+
[reason, runId, taskId]
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
function markAborted(db, runId, taskId, reason, now = Date.now()) {
|
|
63
|
+
requireStatus(db, runId, taskId, ["running", "ready"], "aborted");
|
|
64
|
+
db.run(
|
|
65
|
+
`UPDATE tasks
|
|
66
|
+
SET status='aborted', finished_at=?, last_error=?
|
|
67
|
+
WHERE run_id=? AND task_id=?`,
|
|
68
|
+
[now, reason, runId, taskId]
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function markPending(db, runId, taskId) {
|
|
72
|
+
const cur = getTask(db, runId, taskId);
|
|
73
|
+
if (!cur) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`markPending: task ${JSON.stringify(taskId)} not found in run ${JSON.stringify(runId)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
db.run(
|
|
79
|
+
`UPDATE tasks
|
|
80
|
+
SET status='pending',
|
|
81
|
+
session_id=NULL,
|
|
82
|
+
branch=NULL,
|
|
83
|
+
worktree_path=NULL,
|
|
84
|
+
started_at=NULL,
|
|
85
|
+
finished_at=NULL,
|
|
86
|
+
last_error=NULL
|
|
87
|
+
WHERE run_id=? AND task_id=?`,
|
|
88
|
+
[runId, taskId]
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function setCostUsd(db, runId, taskId, costUsd) {
|
|
92
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
93
|
+
throw new RangeError(`setCostUsd: invalid cost ${costUsd}`);
|
|
94
|
+
}
|
|
95
|
+
db.run(
|
|
96
|
+
"UPDATE tasks SET cost_usd=? WHERE run_id=? AND task_id=?",
|
|
97
|
+
[costUsd, runId, taskId]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function getTask(db, runId, taskId) {
|
|
101
|
+
return db.query("SELECT * FROM tasks WHERE run_id=? AND task_id=?").get(runId, taskId);
|
|
102
|
+
}
|
|
103
|
+
function listTasks(db, runId) {
|
|
104
|
+
return db.query("SELECT * FROM tasks WHERE run_id=? ORDER BY task_id").all(runId);
|
|
105
|
+
}
|
|
106
|
+
function readyTasks(db, runId) {
|
|
107
|
+
return db.query("SELECT * FROM tasks WHERE run_id=? AND status='ready' ORDER BY task_id").all(runId);
|
|
108
|
+
}
|
|
109
|
+
function countByStatus(db, runId) {
|
|
110
|
+
const rows = db.query("SELECT status, COUNT(*) as n FROM tasks WHERE run_id=? GROUP BY status").all(runId);
|
|
111
|
+
const out = {
|
|
112
|
+
pending: 0,
|
|
113
|
+
ready: 0,
|
|
114
|
+
running: 0,
|
|
115
|
+
succeeded: 0,
|
|
116
|
+
failed: 0,
|
|
117
|
+
blocked: 0,
|
|
118
|
+
aborted: 0
|
|
119
|
+
};
|
|
120
|
+
for (const r of rows) out[r.status] = r.n;
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function resetTasksForResume(db, runId) {
|
|
124
|
+
const rows = listTasks(db, runId);
|
|
125
|
+
const resettable = rows.filter((r) => r.status !== "succeeded");
|
|
126
|
+
if (resettable.length === 0) return [];
|
|
127
|
+
const stmt = db.prepare(
|
|
128
|
+
`UPDATE tasks
|
|
129
|
+
SET status='pending',
|
|
130
|
+
attempts=0,
|
|
131
|
+
session_id=NULL,
|
|
132
|
+
last_error=NULL,
|
|
133
|
+
started_at=NULL,
|
|
134
|
+
finished_at=NULL,
|
|
135
|
+
branch=NULL,
|
|
136
|
+
worktree_path=NULL
|
|
137
|
+
WHERE run_id=? AND task_id=? AND status != 'succeeded'`
|
|
138
|
+
);
|
|
139
|
+
const tx = db.transaction(() => {
|
|
140
|
+
for (const r of resettable) stmt.run(runId, r.task_id);
|
|
141
|
+
});
|
|
142
|
+
tx();
|
|
143
|
+
return resettable.map((r) => r.task_id);
|
|
144
|
+
}
|
|
145
|
+
function requireStatus(db, runId, taskId, expected, intended) {
|
|
146
|
+
const row = getTask(db, runId, taskId);
|
|
147
|
+
if (!row) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`task ${JSON.stringify(taskId)} not found in run ${JSON.stringify(runId)}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (!expected.includes(row.status)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`cannot move task ${JSON.stringify(taskId)} from ${row.status} to ${intended} (expected one of: ${expected.join(", ")})`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
upsertFromPlan,
|
|
161
|
+
markReady,
|
|
162
|
+
markRunning,
|
|
163
|
+
markSucceeded,
|
|
164
|
+
markFailed,
|
|
165
|
+
markBlocked,
|
|
166
|
+
markAborted,
|
|
167
|
+
markPending,
|
|
168
|
+
setCostUsd,
|
|
169
|
+
getTask,
|
|
170
|
+
listTasks,
|
|
171
|
+
readyTasks,
|
|
172
|
+
countByStatus,
|
|
173
|
+
resetTasksForResume
|
|
174
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/pilot/state/runs.ts
|
|
2
|
+
import { ulid } from "ulid";
|
|
3
|
+
function createRun(db, args) {
|
|
4
|
+
const id = ulid();
|
|
5
|
+
const now = args.now ?? Date.now();
|
|
6
|
+
db.run(
|
|
7
|
+
`INSERT INTO runs (id, plan_path, plan_slug, started_at, status)
|
|
8
|
+
VALUES (?, ?, ?, ?, 'pending')`,
|
|
9
|
+
[id, args.planPath, args.slug, now]
|
|
10
|
+
);
|
|
11
|
+
void args.plan;
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
function markRunRunning(db, runId) {
|
|
15
|
+
const cur = getRun(db, runId);
|
|
16
|
+
if (!cur) throw new Error(`markRunRunning: run ${JSON.stringify(runId)} not found`);
|
|
17
|
+
if (cur.status === "running") return;
|
|
18
|
+
if (cur.status !== "pending") {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`markRunRunning: cannot move run ${JSON.stringify(runId)} from ${cur.status} to running`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
db.run("UPDATE runs SET status='running' WHERE id=?", [runId]);
|
|
24
|
+
}
|
|
25
|
+
function markRunFinished(db, runId, status, now = Date.now()) {
|
|
26
|
+
if (status !== "completed" && status !== "aborted" && status !== "failed") {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`markRunFinished: ${JSON.stringify(status)} is not a terminal status`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const cur = getRun(db, runId);
|
|
32
|
+
if (!cur) {
|
|
33
|
+
throw new Error(`markRunFinished: run ${JSON.stringify(runId)} not found`);
|
|
34
|
+
}
|
|
35
|
+
db.run("UPDATE runs SET status=?, finished_at=? WHERE id=?", [status, now, runId]);
|
|
36
|
+
}
|
|
37
|
+
function markRunResumed(db, runId) {
|
|
38
|
+
const cur = getRun(db, runId);
|
|
39
|
+
if (!cur) throw new Error(`markRunResumed: run ${JSON.stringify(runId)} not found`);
|
|
40
|
+
if (cur.status === "completed") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`markRunResumed: run ${JSON.stringify(runId)} is already completed; nothing to resume`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
db.run("UPDATE runs SET status='running', finished_at=NULL WHERE id=?", [runId]);
|
|
46
|
+
}
|
|
47
|
+
function getRun(db, runId) {
|
|
48
|
+
const row = db.query("SELECT * FROM runs WHERE id=?").get(runId);
|
|
49
|
+
return row;
|
|
50
|
+
}
|
|
51
|
+
function listRuns(db, limit = 100) {
|
|
52
|
+
return db.query("SELECT * FROM runs ORDER BY started_at DESC LIMIT ?").all(limit);
|
|
53
|
+
}
|
|
54
|
+
function latestRun(db) {
|
|
55
|
+
const row = db.query("SELECT * FROM runs ORDER BY started_at DESC LIMIT 1").get();
|
|
56
|
+
return row;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
createRun,
|
|
61
|
+
markRunRunning,
|
|
62
|
+
markRunFinished,
|
|
63
|
+
markRunResumed,
|
|
64
|
+
getRun,
|
|
65
|
+
listRuns,
|
|
66
|
+
latestRun
|
|
67
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/pilot/paths.ts
|
|
2
|
+
import { promises as fs2 } from "fs";
|
|
3
|
+
import * as os2 from "os";
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
|
|
6
|
+
// src/plan-paths.ts
|
|
7
|
+
import { execFile } from "child_process";
|
|
8
|
+
import * as fs from "fs/promises";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
function execFileP(file, args, opts = {}) {
|
|
12
|
+
const { cwd, timeoutMs = 5e3 } = opts;
|
|
13
|
+
return new Promise((resolve2, reject) => {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
execFile(
|
|
17
|
+
file,
|
|
18
|
+
args,
|
|
19
|
+
{ signal: controller.signal, cwd, encoding: "utf8" },
|
|
20
|
+
(err, stdout) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
if (err) {
|
|
23
|
+
reject(err);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve2(stdout ?? "");
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function expandTilde(p) {
|
|
32
|
+
if (p === "~") return os.homedir();
|
|
33
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
async function getRepoFolder(worktreeDir) {
|
|
37
|
+
let stdout;
|
|
38
|
+
try {
|
|
39
|
+
stdout = await execFileP(
|
|
40
|
+
"git",
|
|
41
|
+
["rev-parse", "--git-common-dir"],
|
|
42
|
+
{ cwd: worktreeDir }
|
|
43
|
+
);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : "unknown error running `git rev-parse --git-common-dir`";
|
|
46
|
+
throw new Error(
|
|
47
|
+
`getRepoFolder: failed to resolve git-common-dir for ${worktreeDir}: ${msg}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const gitCommonDir = stdout.trim();
|
|
51
|
+
if (!gitCommonDir) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`getRepoFolder: \`git rev-parse --git-common-dir\` returned empty for ${worktreeDir}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const absCommonDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(worktreeDir, gitCommonDir);
|
|
57
|
+
const repoRoot = path.dirname(absCommonDir);
|
|
58
|
+
return path.basename(repoRoot);
|
|
59
|
+
}
|
|
60
|
+
async function getPlanDir(worktreeDir) {
|
|
61
|
+
const override = process.env.GLORIOUS_PLAN_DIR;
|
|
62
|
+
const base = override ? expandTilde(override) : path.join(os.homedir(), ".glorious", "opencode");
|
|
63
|
+
const repoFolder = await getRepoFolder(worktreeDir);
|
|
64
|
+
const planDir = path.join(base, repoFolder, "plans");
|
|
65
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
66
|
+
return planDir;
|
|
67
|
+
}
|
|
68
|
+
async function migratePlans(worktreeDir, planDir) {
|
|
69
|
+
const oldDir = path.join(worktreeDir, ".agent", "plans");
|
|
70
|
+
const marker = path.join(oldDir, ".migrated");
|
|
71
|
+
try {
|
|
72
|
+
await fs.stat(oldDir);
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await fs.stat(marker);
|
|
78
|
+
return;
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = await fs.readdir(oldDir);
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const planFiles = entries.filter(
|
|
88
|
+
(name) => name.endsWith(".md") && !name.startsWith(".")
|
|
89
|
+
);
|
|
90
|
+
await fs.mkdir(planDir, { recursive: true });
|
|
91
|
+
for (const name of planFiles) {
|
|
92
|
+
const src = path.join(oldDir, name);
|
|
93
|
+
const dst = path.join(planDir, name);
|
|
94
|
+
let dstExists = false;
|
|
95
|
+
try {
|
|
96
|
+
await fs.stat(dst);
|
|
97
|
+
dstExists = true;
|
|
98
|
+
} catch {
|
|
99
|
+
dstExists = false;
|
|
100
|
+
}
|
|
101
|
+
if (!dstExists) {
|
|
102
|
+
await fs.rename(src, dst);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const [srcBuf, dstBuf] = await Promise.all([
|
|
106
|
+
fs.readFile(src),
|
|
107
|
+
fs.readFile(dst)
|
|
108
|
+
]);
|
|
109
|
+
if (srcBuf.equals(dstBuf)) {
|
|
110
|
+
await fs.unlink(src);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
115
|
+
`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
await fs.writeFile(marker, "");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/pilot/paths.ts
|
|
122
|
+
function expandTilde2(p) {
|
|
123
|
+
if (p === "~") return os2.homedir();
|
|
124
|
+
if (p.startsWith("~/")) return path2.join(os2.homedir(), p.slice(2));
|
|
125
|
+
return p;
|
|
126
|
+
}
|
|
127
|
+
function resolveBaseDir() {
|
|
128
|
+
const pilotEnv = process.env.GLORIOUS_PILOT_DIR;
|
|
129
|
+
if (pilotEnv) return expandTilde2(pilotEnv);
|
|
130
|
+
const planEnv = process.env.GLORIOUS_PLAN_DIR;
|
|
131
|
+
if (planEnv) {
|
|
132
|
+
return path2.dirname(expandTilde2(planEnv));
|
|
133
|
+
}
|
|
134
|
+
return path2.join(os2.homedir(), ".glorious", "opencode");
|
|
135
|
+
}
|
|
136
|
+
async function getPilotDir(cwd) {
|
|
137
|
+
const base = resolveBaseDir();
|
|
138
|
+
const repoFolder = await getRepoFolder(cwd);
|
|
139
|
+
const dir = path2.join(base, repoFolder, "pilot");
|
|
140
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
141
|
+
return dir;
|
|
142
|
+
}
|
|
143
|
+
async function getPlansDir(cwd) {
|
|
144
|
+
const pilot = await getPilotDir(cwd);
|
|
145
|
+
const dir = path2.join(pilot, "plans");
|
|
146
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
147
|
+
return dir;
|
|
148
|
+
}
|
|
149
|
+
async function getRunDir(cwd, runId) {
|
|
150
|
+
if (!isSafeRunId(runId)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`getRunDir: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const pilot = await getPilotDir(cwd);
|
|
156
|
+
const dir = path2.join(pilot, "runs", runId);
|
|
157
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
158
|
+
return dir;
|
|
159
|
+
}
|
|
160
|
+
async function getStateDbPath(cwd, runId) {
|
|
161
|
+
const runDir = await getRunDir(cwd, runId);
|
|
162
|
+
return path2.join(runDir, "state.db");
|
|
163
|
+
}
|
|
164
|
+
async function getWorkerJsonlPath(cwd, runId, n) {
|
|
165
|
+
const runDir = await getRunDir(cwd, runId);
|
|
166
|
+
const workersDir = path2.join(runDir, "workers");
|
|
167
|
+
await fs2.mkdir(workersDir, { recursive: true });
|
|
168
|
+
const padded = n.toString().padStart(2, "0");
|
|
169
|
+
return path2.join(workersDir, `${padded}.jsonl`);
|
|
170
|
+
}
|
|
171
|
+
async function getTaskJsonlPath(cwd, runId, taskId) {
|
|
172
|
+
if (!isSafeRunId(runId)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`getTaskJsonlPath: runId ${JSON.stringify(runId)} is not a safe filesystem segment`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!isSafeTaskId(taskId)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`getTaskJsonlPath: taskId ${JSON.stringify(taskId)} is not a safe filesystem segment`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const runDir = await getRunDir(cwd, runId);
|
|
183
|
+
const taskDir = path2.join(runDir, "tasks", taskId);
|
|
184
|
+
await fs2.mkdir(taskDir, { recursive: true });
|
|
185
|
+
return path2.join(taskDir, "session.jsonl");
|
|
186
|
+
}
|
|
187
|
+
function isSafeRunId(runId) {
|
|
188
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(runId);
|
|
189
|
+
}
|
|
190
|
+
function isSafeTaskId(taskId) {
|
|
191
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(taskId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
getPlanDir,
|
|
196
|
+
migratePlans,
|
|
197
|
+
resolveBaseDir,
|
|
198
|
+
getPilotDir,
|
|
199
|
+
getPlansDir,
|
|
200
|
+
getRunDir,
|
|
201
|
+
getStateDbPath,
|
|
202
|
+
getWorkerJsonlPath,
|
|
203
|
+
getTaskJsonlPath
|
|
204
|
+
};
|