@stravica-ai/digital-operator-pa 0.1.48 → 0.1.50
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/dist/engineering-pa-cli/src/pa/cli.mjs +71 -2
- package/dist/engineering-pa-cli/src/pa/onboard.mjs +114 -0
- package/dist/engineering-pa-core/docs/commands/onboard.md +101 -27
- package/dist/engineering-pa-core/src/distill/cli.mjs +65 -0
- package/dist/engineering-pa-core/src/distill/health.mjs +486 -0
- package/dist/engineering-pa-core/src/distill/run.mjs +19 -0
- package/dist/engineering-pa-core/src/instance-root/find.mjs +104 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/backlink-indexer.mjs +6 -1
- package/dist/engineering-pa-core/src/knowledge/retrieval/graph-builder.mjs +6 -1
- package/dist/engineering-pa-core/src/knowledge/retrieval/index-builder.mjs +4 -1
- package/dist/engineering-pa-core/src/knowledge/retrieval/query.mjs +6 -1
- package/dist/engineering-pa-core/src/scheduler/install.mjs +41 -4
- package/dist/engineering-pa-core/src/scheduler/list.mjs +34 -13
- package/dist/engineering-pa-core/src/scheduler/remove.mjs +2 -1
- package/dist/engineering-pa-core-extensions-atlassian/package.json +3 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +4 -2
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +4 -2
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +6 -2
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +7 -3
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +9 -3
- package/dist/engineering-pa-scaffold/src/generator.mjs +222 -11
- package/dist/engineering-pa-workspace/app/notes.html +1 -1
- package/dist/engineering-pa-workspace/app/project-detail.html +17 -1
- package/dist/engineering-pa-workspace/app/projects.html +1 -1
- package/dist/engineering-pa-workspace/app/tasks.html +1 -1
- package/dist/engineering-pa-workspace/src/install-daemon.mjs +2 -2
- package/dist/engineering-pa-workspace/src/repo-root.mjs +13 -100
- package/dist/engineering-pa-workspace/src/server.mjs +2 -2
- package/dist/engineering-pa-workspace/src/ws.mjs +79 -10
- package/docs/onboarding-solo.md +5 -5
- package/package.json +1 -1
|
@@ -11,7 +11,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { run as runDesk } from '../../../engineering-pa-core/src/desk/cli.mjs';
|
|
12
12
|
import { PA_PACKAGE_NAME } from '../../../engineering-pa-core/src/config/pa-package-name.mjs';
|
|
13
13
|
import { readPreferencesSync } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
14
|
+
import {
|
|
15
|
+
computeDistillationHealth,
|
|
16
|
+
renderHealthLine,
|
|
17
|
+
} from '../../../engineering-pa-core/src/distill/health.mjs';
|
|
14
18
|
import { main as runMigrateInspect } from '../../../engineering-pa-migrate-inspect/src/cli.mjs';
|
|
19
|
+
import { doOnboard } from './onboard.mjs';
|
|
15
20
|
|
|
16
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
22
|
|
|
@@ -34,6 +39,7 @@ Usage:
|
|
|
34
39
|
pa migrate-inspect [options] Host inspection for migration planning
|
|
35
40
|
pa launch --agent=<n> --task=<id> [--dry-run]
|
|
36
41
|
Boot a scoped agent instance bound to an identity and task
|
|
42
|
+
pa onboard <verb> Onboarding walkthrough state (complete)
|
|
37
43
|
pa -h, --help Show this help
|
|
38
44
|
|
|
39
45
|
Routed sub-CLIs (also available as standalone bins):
|
|
@@ -88,6 +94,8 @@ export async function main(argv) {
|
|
|
88
94
|
return await runMigrateInspect(rest);
|
|
89
95
|
case 'launch':
|
|
90
96
|
return await doLaunch(rest);
|
|
97
|
+
case 'onboard':
|
|
98
|
+
return await doOnboard(rest);
|
|
91
99
|
default:
|
|
92
100
|
process.stderr.write(`[pa] unknown verb: ${verb}\n`);
|
|
93
101
|
process.stderr.write(USAGE);
|
|
@@ -170,17 +178,47 @@ async function doStatus(opts = {}) {
|
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
// 3. Workspace reachability probe.
|
|
181
|
+
//
|
|
182
|
+
// 0.1.50 (item 12): report BOTH the running port (from `.ws.pid` when
|
|
183
|
+
// present) and the configured port when they differ, so the operator
|
|
184
|
+
// can see when the auto-probe moved the server onto a different port
|
|
185
|
+
// than preferences declare. Probe the running port when available,
|
|
186
|
+
// falling back to the configured port.
|
|
173
187
|
out.write('\nWorkspace:\n');
|
|
174
|
-
const
|
|
188
|
+
const runningPort = readRunningWorkspacePort(repoRoot);
|
|
189
|
+
const effectivePort = runningPort ?? workspacePort;
|
|
190
|
+
const probeUrl = `http://localhost:${effectivePort}/__api/identity`;
|
|
175
191
|
const reachability = await probeWorkspace(probeUrl, opts.fetchImpl);
|
|
176
192
|
out.write(` ${probeUrl}\n`);
|
|
193
|
+
if (runningPort !== null && runningPort !== workspacePort) {
|
|
194
|
+
out.write(` running port: ${runningPort} (configured: ${workspacePort})\n`);
|
|
195
|
+
}
|
|
177
196
|
out.write(` status: ${reachability.status}`);
|
|
178
197
|
if (reachability.detail) out.write(` (${reachability.detail})`);
|
|
179
198
|
out.write('\n');
|
|
180
199
|
// Workspace-not-running is OK — operators may run pa status before
|
|
181
200
|
// starting the workspace server.
|
|
182
201
|
|
|
183
|
-
// 4.
|
|
202
|
+
// 4. Distillation health — schedule installed?, last run, staging
|
|
203
|
+
// count. Load-bearing because the whole self-learning loop dies
|
|
204
|
+
// silently otherwise (the estate has already been bitten: on the
|
|
205
|
+
// WSD-laptop cold-install proof of 0.1.41, staged observations piled
|
|
206
|
+
// up for days without a scheduled sweep to distil them). Rendered on
|
|
207
|
+
// one line, prefixed `[ok]` or `[warn]` per the health module.
|
|
208
|
+
out.write('\nDistillation:\n');
|
|
209
|
+
try {
|
|
210
|
+
const health = await computeDistillationHealth({ repoRoot });
|
|
211
|
+
out.write(` ${renderHealthLine(health)}\n`);
|
|
212
|
+
// Do NOT flip allOk here — a fresh scaffold with no schedule
|
|
213
|
+
// installed (the exact moment /onboard is meant to run) would then
|
|
214
|
+
// exit 2 from pa status, which is the pre-install check for the
|
|
215
|
+
// scaffold itself. Warnings are visible in the line; exit code is
|
|
216
|
+
// reserved for structural failures (missing operator/).
|
|
217
|
+
} catch (e) {
|
|
218
|
+
out.write(` distillation health probe failed: ${e?.message || e}\n`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 5. Desk overview snapshot.
|
|
184
222
|
out.write('\nOverview:\n');
|
|
185
223
|
try {
|
|
186
224
|
const code = await runDesk(['overview']);
|
|
@@ -196,6 +234,37 @@ async function doStatus(opts = {}) {
|
|
|
196
234
|
return allOk ? 0 : 2;
|
|
197
235
|
}
|
|
198
236
|
|
|
237
|
+
/**
|
|
238
|
+
* 0.1.50 (item 12): read the port the workspace is currently listening
|
|
239
|
+
* on from `.ws.pid`. Returns null when the pidfile is absent, corrupt,
|
|
240
|
+
* or the recorded PID is not alive. Never throws — callers fall back
|
|
241
|
+
* to the configured port silently.
|
|
242
|
+
*
|
|
243
|
+
* `.ws.pid` is written by `ws.mjs::cmdStart` on foreground spawn.
|
|
244
|
+
* Supervised installs (launchd / systemd) do NOT write it; when that is
|
|
245
|
+
* the case we still fall back to the configured port and the reachability
|
|
246
|
+
* probe hits the identity endpoint from there.
|
|
247
|
+
*/
|
|
248
|
+
function readRunningWorkspacePort(repoRoot) {
|
|
249
|
+
try {
|
|
250
|
+
const pidPath = join(repoRoot, '.ws.pid');
|
|
251
|
+
if (!existsSync(pidPath)) return null;
|
|
252
|
+
const raw = JSON.parse(readFileSync(pidPath, 'utf8'));
|
|
253
|
+
if (!raw || typeof raw.pid !== 'number' || typeof raw.port !== 'number') return null;
|
|
254
|
+
// Verify the process is alive before trusting the port. A stale
|
|
255
|
+
// pidfile pointing at a dead process should not surface a
|
|
256
|
+
// running-port line.
|
|
257
|
+
try {
|
|
258
|
+
process.kill(raw.pid, 0);
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return raw.port;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
199
268
|
function resolveWorkspacePort(repoRoot, opts = {}) {
|
|
200
269
|
if (opts.workspacePort) return opts.workspacePort;
|
|
201
270
|
try {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// onboard.mjs — `pa onboard` verb, currently just the `complete` subcommand.
|
|
2
|
+
//
|
|
3
|
+
// Adds a completion guard to the /onboard walkthrough. `.session/onboarding.json`
|
|
4
|
+
// carries per-step state; hand-editing it to set `completedAt` (the pre-0.1.50
|
|
5
|
+
// pattern) skipped the mandatory-step check. This verb refuses when a mandatory
|
|
6
|
+
// step is not `done`, names it, and quotes the remediation. When every mandatory
|
|
7
|
+
// step is done it stamps `completedAt` with an ISO string (UTC, second-precision)
|
|
8
|
+
// and preserves the 2-space indent of the file so subsequent diffs stay clean.
|
|
9
|
+
//
|
|
10
|
+
// Mandatory steps (as of 0.1.50): librarianSchedule. See
|
|
11
|
+
// packages/core/docs/commands/onboard.md §Completion.
|
|
12
|
+
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { findRepoRoot } from './repo-root.mjs';
|
|
17
|
+
|
|
18
|
+
const HELP = `pa onboard - Onboarding walkthrough state
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
pa onboard complete Guard-checked completion of /onboard
|
|
22
|
+
pa onboard -h, --help Show this help
|
|
23
|
+
|
|
24
|
+
Subcommands:
|
|
25
|
+
complete
|
|
26
|
+
Reads .session/onboarding.json, refuses when any mandatory step (currently
|
|
27
|
+
librarianSchedule) is not 'done', otherwise stamps completedAt.
|
|
28
|
+
|
|
29
|
+
Exit codes:
|
|
30
|
+
0 ok | 1 usage or a mandatory step is pending | 2 IO failure
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const MANDATORY_STEPS = [
|
|
34
|
+
{
|
|
35
|
+
id: 'librarianSchedule',
|
|
36
|
+
remediation: 'run /onboard and complete the librarianSchedule step (installs the mandatory distillation sweep)',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export async function doOnboard(argv) {
|
|
41
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
42
|
+
process.stdout.write(HELP);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const sub = argv[0];
|
|
46
|
+
const rest = argv.slice(1);
|
|
47
|
+
switch (sub) {
|
|
48
|
+
case 'complete':
|
|
49
|
+
return await onboardComplete(rest);
|
|
50
|
+
default:
|
|
51
|
+
process.stderr.write(`[pa onboard] unknown subcommand: ${sub}\n`);
|
|
52
|
+
process.stderr.write(HELP);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run the completion guard against `.session/onboarding.json`. Reads the
|
|
59
|
+
* file, checks every mandatory step is `done`, and stamps `completedAt`
|
|
60
|
+
* to the current UTC ISO timestamp (millisecond-truncated) when the
|
|
61
|
+
* gate passes.
|
|
62
|
+
*
|
|
63
|
+
* @param {string[]} argv reserved; no flags today
|
|
64
|
+
* @returns {Promise<number>} exit code
|
|
65
|
+
*/
|
|
66
|
+
export async function onboardComplete(argv, deps = {}) {
|
|
67
|
+
const stderr = deps.stderr || process.stderr;
|
|
68
|
+
const stdout = deps.stdout || process.stdout;
|
|
69
|
+
const now = deps.now || (() => new Date().toISOString());
|
|
70
|
+
const repoRoot = deps.repoRoot
|
|
71
|
+
|| process.env.PA_REPO_ROOT
|
|
72
|
+
|| process.env.DAVE_REPO_ROOT
|
|
73
|
+
|| await findRepoRoot(process.cwd());
|
|
74
|
+
if (!repoRoot) {
|
|
75
|
+
stderr.write('[pa onboard complete] could not resolve the operator repo root — set PA_REPO_ROOT or run from inside the instance.\n');
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
const path = join(repoRoot, '.session', 'onboarding.json');
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = await readFile(path, 'utf8');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e && e.code === 'ENOENT') {
|
|
84
|
+
stderr.write(`[pa onboard complete] no onboarding state at ${path} — run /onboard first.\n`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
stderr.write(`[pa onboard complete] failed to read ${path}: ${e.message}\n`);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(raw);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
stderr.write(`[pa onboard complete] onboarding state at ${path} is not valid JSON: ${e.message}\n`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
const steps = parsed.steps || {};
|
|
98
|
+
const pending = [];
|
|
99
|
+
for (const step of MANDATORY_STEPS) {
|
|
100
|
+
if (steps[step.id] !== 'done') pending.push(step);
|
|
101
|
+
}
|
|
102
|
+
if (pending.length > 0) {
|
|
103
|
+
for (const step of pending) {
|
|
104
|
+
stderr.write(`[pa onboard complete] mandatory step '${step.id}' is not done (currently: ${JSON.stringify(steps[step.id] ?? null)})\n`);
|
|
105
|
+
stderr.write(` Remediation: ${step.remediation}\n`);
|
|
106
|
+
}
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
parsed.completedAt = now();
|
|
110
|
+
const hadTrailingNewline = raw.endsWith('\n');
|
|
111
|
+
await writeFile(path, JSON.stringify(parsed, null, 2) + (hadTrailingNewline ? '\n' : ''));
|
|
112
|
+
stdout.write(`Onboarding marked complete at ${parsed.completedAt}\n`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
@@ -28,17 +28,21 @@ Read `.session/onboarding.json` first. Shape:
|
|
|
28
28
|
"version": 1,
|
|
29
29
|
"startedAt": "<iso>",
|
|
30
30
|
"steps": {
|
|
31
|
-
"verifyRuntime":
|
|
32
|
-
"verifyVersion":
|
|
33
|
-
"profileIntake":
|
|
34
|
-
"aliasOffer":
|
|
35
|
-
"workspaceOffer":"pending" | "done" | "skipped",
|
|
36
|
-
"
|
|
31
|
+
"verifyRuntime": "pending" | "done" | "skipped",
|
|
32
|
+
"verifyVersion": "pending" | "done" | "skipped",
|
|
33
|
+
"profileIntake": "pending" | "done" | "skipped",
|
|
34
|
+
"aliasOffer": "pending" | "done" | "skipped",
|
|
35
|
+
"workspaceOffer": "pending" | "done" | "skipped",
|
|
36
|
+
"librarianSchedule": "pending" | "done",
|
|
37
|
+
"atlassianOffer": "pending" | "done" | "skipped"
|
|
37
38
|
},
|
|
38
39
|
"completedAt": null | "<iso>"
|
|
39
40
|
}
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
`librarianSchedule` never lands on `skipped`. It is a mandatory step and
|
|
44
|
+
onboarding does not complete until it is `done`.
|
|
45
|
+
|
|
42
46
|
If the file is missing, the scaffold did not seed it — flag that and stop.
|
|
43
47
|
|
|
44
48
|
If `completedAt` is set, tell the operator onboarding is already complete and
|
|
@@ -93,28 +97,92 @@ line to run; do not run it for them (their shell rc is not our file to write).
|
|
|
93
97
|
### 5. workspaceOffer
|
|
94
98
|
|
|
95
99
|
Ask if they want the local workspace started now. On yes, run
|
|
96
|
-
`pnpm exec pa workspace start` in the background
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`"workspacePort": N` so subsequent sessions start on the same port
|
|
108
|
-
without asking again. Preserve the rest of the file exactly, and
|
|
109
|
-
round-trip through `JSON.parse` / `JSON.stringify` with a two-space
|
|
110
|
-
indent to keep the on-disk shape stable.
|
|
111
|
-
4. Tell the operator you moved the workspace off 4200 and named the
|
|
112
|
-
port that stuck.
|
|
100
|
+
`pnpm exec pa workspace start` in the background. The CLI auto-probes
|
|
101
|
+
for a free port when the configured one is busy (starting at
|
|
102
|
+
max(configured+1, 4210), up to 20 attempts) and persists the chosen
|
|
103
|
+
port to `operator/config/preferences.json:workspacePort`, so no manual
|
|
104
|
+
port dance is needed. Print the URL from the resulting preferences
|
|
105
|
+
file (the workspace start command's own output also names the chosen
|
|
106
|
+
port).
|
|
107
|
+
|
|
108
|
+
Pass `--port <N>` when the operator wants a specific port, or
|
|
109
|
+
`--no-probe` to keep the strict hard-fail behaviour instead of the
|
|
110
|
+
auto-probe.
|
|
113
111
|
|
|
114
112
|
On no, note that `pnpm exec pa workspace start` is the verb whenever
|
|
115
113
|
they want it.
|
|
116
114
|
|
|
117
|
-
### 6.
|
|
115
|
+
### 6. librarianSchedule (mandatory, no opt-out)
|
|
116
|
+
|
|
117
|
+
The distillation sweep is what turns staged observations under
|
|
118
|
+
`agents/<paName>/staging/` into knowledge docs under
|
|
119
|
+
`operator/knowledge/`. Without a scheduled sweep the whole self-learning
|
|
120
|
+
loop dies silently: staged notes pile up, no docs get promoted, and
|
|
121
|
+
`pa knowledge search` never sees the new material. The scaffold ships
|
|
122
|
+
the schedule file at `schedules/<paName>-distillation-sweep.json`; this
|
|
123
|
+
step installs it on the OS-native backend and fires one real run to
|
|
124
|
+
prove the wiring.
|
|
125
|
+
|
|
126
|
+
Do NOT ask the operator whether to do this. State what is happening in
|
|
127
|
+
one short paragraph:
|
|
128
|
+
|
|
129
|
+
> Installing the librarian distillation sweep so your Digital Operator
|
|
130
|
+
> keeps learning. It runs every four hours in the background, turns your
|
|
131
|
+
> staged observations into knowledge docs, and refreshes the retrieval
|
|
132
|
+
> graph so the new material is searchable straight away. This is the
|
|
133
|
+
> one piece of scheduling every solo instance needs.
|
|
134
|
+
|
|
135
|
+
Then run:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
pnpm exec scheduler install schedules/<paName>-distillation-sweep.json --force-host
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Substitute `<paName>` from `operator/config/preferences.json`. The
|
|
142
|
+
`--force-host` is deliberate. macOS hostnames flap between the DHCP
|
|
143
|
+
name and the configured name (Dex 2026-07-16 evidence), and a well-
|
|
144
|
+
formed refusal at first-run onboarding is the worst possible operator
|
|
145
|
+
experience.
|
|
146
|
+
|
|
147
|
+
Immediately after install, trigger one real sweep through the scheduler
|
|
148
|
+
backend itself (never a shell repro: a shell resolves `node` and PATH
|
|
149
|
+
differently and would hide the launchd/cron spawn behaviour the ledger
|
|
150
|
+
is meant to prove). On macOS/launchd:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
launchctl kickstart -k "gui/$(id -u)/com.<paName>.<paName>-distillation-sweep"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
On Linux/cron, invoke the exact command line the crontab entry carries,
|
|
157
|
+
from the same working directory, with a minimal PATH the cron job would
|
|
158
|
+
see:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
env -i HOME="$HOME" PATH=/usr/local/bin:/usr/bin:/bin \
|
|
162
|
+
bash -c 'cd <targetDir> && node node_modules/@stravica-ai/digital-operator-pa/bin/distill run'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Wait for the run with a hard timeout of 6 minutes (a real sweep with
|
|
166
|
+
one `claude --print` subprocess is typically 60–120 seconds, but the
|
|
167
|
+
first-ever run on a cold cache can stretch). Poll
|
|
168
|
+
`.cache/distill/logs/runs.jsonl`. A completed run appends one JSON line
|
|
169
|
+
with `"status": "ok"` or `"status": "noop-empty-staging"`. Either
|
|
170
|
+
status is a successful proof: the pipeline ran end to end.
|
|
171
|
+
|
|
172
|
+
Mark this step `done` only when the ledger shows a completed run. If
|
|
173
|
+
the install fails, or the run times out, or the ledger records a
|
|
174
|
+
`fail-*` status, this step is NOT `done`, onboarding does NOT complete,
|
|
175
|
+
and the operator gets:
|
|
176
|
+
|
|
177
|
+
1. The exact error message (from the scheduler CLI's stderr, or the
|
|
178
|
+
ledger's `reason` field, or a timeout notice).
|
|
179
|
+
2. The one command to retry, verbatim:
|
|
180
|
+
`pnpm exec scheduler install schedules/<paName>-distillation-sweep.json --force-host`
|
|
181
|
+
|
|
182
|
+
Then stop. Do not silently skip to `atlassianOffer`. The whole
|
|
183
|
+
onboarding is blocked on this step.
|
|
184
|
+
|
|
185
|
+
### 7. atlassianOffer
|
|
118
186
|
|
|
119
187
|
Ask if they use Jira or Confluence. On no, mark skipped. On yes, walk them
|
|
120
188
|
through:
|
|
@@ -131,6 +199,12 @@ identity each returns.
|
|
|
131
199
|
|
|
132
200
|
## Completion
|
|
133
201
|
|
|
134
|
-
When every step is done or skipped,
|
|
135
|
-
|
|
136
|
-
|
|
202
|
+
When every step is done or skipped, run `pnpm exec pa onboard complete`.
|
|
203
|
+
The verb refuses when any mandatory step (currently `librarianSchedule`)
|
|
204
|
+
is not `done`, naming the pending step and its remediation; otherwise
|
|
205
|
+
it stamps `completedAt` in `.session/onboarding.json` with a 2-space
|
|
206
|
+
indent. Never hand-edit the file.
|
|
207
|
+
|
|
208
|
+
Tell the operator onboarding is done. Give them one line: "You are
|
|
209
|
+
ready. Ask me anything, or say `bring me up to speed` and I will run
|
|
210
|
+
the standard session-boot briefing."
|
|
@@ -10,12 +10,26 @@ import { parseArgs } from 'node:util';
|
|
|
10
10
|
import { defaultRepoRoot } from './prereqs.mjs';
|
|
11
11
|
import { runDistill } from './run.mjs';
|
|
12
12
|
import { defaultRunner } from './runner.mjs';
|
|
13
|
+
import {
|
|
14
|
+
buildHealthHookEnvelope,
|
|
15
|
+
computeDistillationHealth,
|
|
16
|
+
renderHealthBlock,
|
|
17
|
+
renderHealthLine,
|
|
18
|
+
} from './health.mjs';
|
|
13
19
|
|
|
14
20
|
const USAGE = `distill — librarian distillation sweep runner
|
|
15
21
|
|
|
16
22
|
Usage:
|
|
17
23
|
distill run Run a real distillation sweep
|
|
18
24
|
distill run --dry-run Print what would run, don't invoke claude
|
|
25
|
+
distill health Print one-line schedule + last-run + staging summary
|
|
26
|
+
distill health --json Print the full health object as JSON
|
|
27
|
+
distill health --block Print the multi-line markdown block
|
|
28
|
+
distill health --hook-envelope
|
|
29
|
+
Print a Claude Code SessionStart hook envelope
|
|
30
|
+
(JSON, {hookSpecificOutput: {...}}) — the shape
|
|
31
|
+
scaffolded into .claude/settings.json so the
|
|
32
|
+
summary is injected at session boot.
|
|
19
33
|
distill -h, --help Show this help
|
|
20
34
|
|
|
21
35
|
Exit codes:
|
|
@@ -106,6 +120,57 @@ async function route(argv) {
|
|
|
106
120
|
process.exit(code ?? 0);
|
|
107
121
|
}
|
|
108
122
|
|
|
123
|
+
if (verb === 'health') {
|
|
124
|
+
const { values } = parseArgs({
|
|
125
|
+
args: rest,
|
|
126
|
+
options: {
|
|
127
|
+
json: { type: 'boolean', default: false },
|
|
128
|
+
block: { type: 'boolean', default: false },
|
|
129
|
+
'hook-envelope': { type: 'boolean', default: false },
|
|
130
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
131
|
+
},
|
|
132
|
+
strict: true,
|
|
133
|
+
});
|
|
134
|
+
if (values.help) {
|
|
135
|
+
process.stdout.write(USAGE);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
const repoRoot = await defaultRepoRoot();
|
|
139
|
+
if (!repoRoot) {
|
|
140
|
+
// Preserve exit-code hygiene: no repo root == prerequisite missing.
|
|
141
|
+
// Still emit a valid hook envelope on --hook-envelope so a badly
|
|
142
|
+
// configured Claude session never sees invalid JSON on stdout.
|
|
143
|
+
const fallback = {
|
|
144
|
+
repoRoot: null, paName: null, cadenceHours: 4,
|
|
145
|
+
schedule: { present: false, path: null },
|
|
146
|
+
lastRun: null, stagingCount: { error: 'no repo root resolved' },
|
|
147
|
+
warnings: ['no operator repo root resolved from cwd (no `.git` ancestor and no DAVE_REPO_ROOT / PA_REPO_ROOT set)'],
|
|
148
|
+
ok: false,
|
|
149
|
+
generatedAt: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
if (values['hook-envelope']) {
|
|
152
|
+
process.stdout.write(JSON.stringify(buildHealthHookEnvelope(fallback)) + '\n');
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
if (values.json) process.stdout.write(JSON.stringify(fallback, null, 2) + '\n');
|
|
156
|
+
else if (values.block) process.stdout.write(renderHealthBlock(fallback));
|
|
157
|
+
else process.stdout.write(renderHealthLine(fallback) + '\n');
|
|
158
|
+
process.exit(2);
|
|
159
|
+
}
|
|
160
|
+
const health = await computeDistillationHealth({ repoRoot });
|
|
161
|
+
if (values['hook-envelope']) {
|
|
162
|
+
process.stdout.write(JSON.stringify(buildHealthHookEnvelope(health)) + '\n');
|
|
163
|
+
// Hook path: always exit 0. Failing here would surface as a Claude
|
|
164
|
+
// Code hook error at every session boot, which is worse than a
|
|
165
|
+
// rendered warning block.
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
if (values.json) process.stdout.write(JSON.stringify(health, null, 2) + '\n');
|
|
169
|
+
else if (values.block) process.stdout.write(renderHealthBlock(health));
|
|
170
|
+
else process.stdout.write(renderHealthLine(health) + '\n');
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
109
174
|
process.stderr.write(`[distill] unknown command: ${verb}\n`);
|
|
110
175
|
process.stderr.write(USAGE);
|
|
111
176
|
process.exit(1);
|