@stravica-ai/digital-operator-pa 0.1.51 → 0.1.52
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/work-cli.mjs +5 -0
- package/dist/engineering-pa-cli/src/pa/work.mjs +12 -2
- package/dist/engineering-pa-cli/src/scheduler/cli.mjs +8 -4
- package/dist/engineering-pa-cli/src/secrets/commands/init.mjs +35 -1
- package/dist/engineering-pa-core/docs/commands/onboard.md +6 -4
- package/dist/engineering-pa-core/src/scheduler/install.mjs +1 -1
- package/dist/engineering-pa-core/src/scheduler/list.mjs +41 -26
- package/dist/engineering-pa-core/src/scheduler/remove.mjs +43 -22
- package/dist/engineering-pa-core/src/scheduler/resolve-agent.mjs +56 -0
- package/dist/engineering-pa-workspace/src/api.mjs +6 -1
- package/docs/onboarding-solo.md +34 -7
- package/package.json +1 -1
|
@@ -36,6 +36,11 @@ Usage:
|
|
|
36
36
|
|
|
37
37
|
Notes:
|
|
38
38
|
- status=done stamps \`completedAt\` to today (only if not already set).
|
|
39
|
+
- \`list\` defaults to the active-only scope (excludes done + archived).
|
|
40
|
+
An explicit \`--status <s>\` REPLACES that default and returns only
|
|
41
|
+
items with that status (so \`--status done\` prints done items).
|
|
42
|
+
\`--all\` means no status filter; \`--mine\`/\`--theirs\` compose with
|
|
43
|
+
either. \`--status\` wins over \`--all\` when both are given.
|
|
39
44
|
- --mine resolves the PA name from operator/config/preferences.json
|
|
40
45
|
(paName / agentName). Default: "pa".
|
|
41
46
|
- --author sets the writer-scoped id's author token. Precedence:
|
|
@@ -228,6 +228,15 @@ export async function setWorkStatus(id, status) {
|
|
|
228
228
|
* mine, theirs, all, paName (used for --mine resolution).
|
|
229
229
|
* By default excludes done + archived (mirrors desk-list semantics).
|
|
230
230
|
*
|
|
231
|
+
* An explicit `status` filter REPLACES the active-only default rather
|
|
232
|
+
* than being AND-ed with it: `--status done` returns only done items,
|
|
233
|
+
* `--status archived` returns only archived items, `--status in-progress`
|
|
234
|
+
* is unchanged (the AND was harmless for non-terminal statuses). The
|
|
235
|
+
* pre-0.1.52 AND made any terminal-status request unconditionally
|
|
236
|
+
* empty ("no work items match" for `--status done`), a confident false
|
|
237
|
+
* negative. `--all` still means "no status filter at all"; if both
|
|
238
|
+
* `--all` and `--status` are given, `--status` wins.
|
|
239
|
+
*
|
|
231
240
|
* Reads any well-formed work item (both id forms). Skips files whose
|
|
232
241
|
* filename does not parse as a work-item id at all — the same
|
|
233
242
|
* skip-unparseable posture the original had, restated against the
|
|
@@ -248,10 +257,11 @@ export async function listWork(filters = {}) {
|
|
|
248
257
|
}
|
|
249
258
|
}
|
|
250
259
|
let filtered = items;
|
|
251
|
-
if (
|
|
260
|
+
if (filters.status) {
|
|
261
|
+
filtered = filtered.filter((w) => w.status === filters.status);
|
|
262
|
+
} else if (!filters.all) {
|
|
252
263
|
filtered = filtered.filter((w) => !['done', 'archived'].includes(w.status));
|
|
253
264
|
}
|
|
254
|
-
if (filters.status) filtered = filtered.filter((w) => w.status === filters.status);
|
|
255
265
|
if (filters.category) filtered = filtered.filter((w) => w.category === filters.category);
|
|
256
266
|
if (filters.project) filtered = filtered.filter((w) => w.projectId === filters.project);
|
|
257
267
|
if (filters.owner) filtered = filtered.filter((w) => w.owner === filters.owner);
|
|
@@ -33,10 +33,14 @@ Usage:
|
|
|
33
33
|
Schedule files live at <repoRoot>/schedules/<id>.json. The canonical
|
|
34
34
|
schema is bundled at @stravica-ai/engineering-pa-core/schema/schedule.json.
|
|
35
35
|
|
|
36
|
-
--agent
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
--agent targets a specific agent's label / tag prefix. Install reads
|
|
37
|
+
the agent from the schedule file's own top-level "agent" field. Since
|
|
38
|
+
0.1.52 (w-2026-09-19-dave-001), list/remove resolve the agent from the
|
|
39
|
+
invoking instance's "paName" in operator/config/preferences.json when
|
|
40
|
+
--agent is omitted, and REFUSE (exit 2) when neither source produces a
|
|
41
|
+
value — there is no hard-coded "dave" fallback. This is the fix for the
|
|
42
|
+
colleague-laptop bug where the tools would list and touch another
|
|
43
|
+
estate's schedules under a silent default.
|
|
40
44
|
|
|
41
45
|
Host enforcement (install): a schedule's "host" field is matched against
|
|
42
46
|
this machine's hostname (exact, then short-name-before-first-dot). A
|
|
@@ -85,7 +85,41 @@ function parseStdinJson(raw) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export async function runInit(overrides = {}) {
|
|
88
|
-
|
|
88
|
+
// 0.1.52 fix (w-2026-09-19-dave-001): `secrets init` CREATES an age
|
|
89
|
+
// key. The shared resolver in core/vault/require.mjs honours
|
|
90
|
+
// SOPS_AGE_KEY_FILE first, so with the variable exported the previous
|
|
91
|
+
// init generated the new key AT the env path — a stray key landed
|
|
92
|
+
// outside the instance, and on a colleague laptop that carried
|
|
93
|
+
// another estate's SOPS_AGE_KEY_FILE it would have written the new
|
|
94
|
+
// key into that estate's key file. The env override is a READ-path
|
|
95
|
+
// escape hatch; init must generate at the instance path (solo mode)
|
|
96
|
+
// or the per-user path (HQ mode) regardless of the variable.
|
|
97
|
+
//
|
|
98
|
+
// Strip SOPS_AGE_KEY_FILE from the env passed to `defaultPaths` only
|
|
99
|
+
// — every OTHER env-derived resolution (DAVE_ROOT / PA_ROOT walk,
|
|
100
|
+
// etc.) still uses the caller's real env. Print a one-line notice
|
|
101
|
+
// when the operator had the override set and the resolved
|
|
102
|
+
// key path does not match it, so subsequent reads that continue to
|
|
103
|
+
// consult the env override are not silently pointed at the old file.
|
|
104
|
+
const rawEnv = overrides.env;
|
|
105
|
+
const envOverride = rawEnv && typeof rawEnv.SOPS_AGE_KEY_FILE === 'string' && rawEnv.SOPS_AGE_KEY_FILE.length > 0
|
|
106
|
+
? rawEnv.SOPS_AGE_KEY_FILE
|
|
107
|
+
: null;
|
|
108
|
+
let envForPaths = rawEnv;
|
|
109
|
+
if (envOverride !== null && !overrides.paths) {
|
|
110
|
+
envForPaths = { ...rawEnv };
|
|
111
|
+
delete envForPaths.SOPS_AGE_KEY_FILE;
|
|
112
|
+
}
|
|
113
|
+
const paths = overrides.paths || defaultPaths({
|
|
114
|
+
...overrides,
|
|
115
|
+
env: envForPaths,
|
|
116
|
+
allowGitRootWithoutVault: true,
|
|
117
|
+
});
|
|
118
|
+
if (envOverride !== null && envOverride !== paths.keyFile) {
|
|
119
|
+
console.log(
|
|
120
|
+
`Note: SOPS_AGE_KEY_FILE is set to ${envOverride}; init writes the new key to ${paths.keyFile}. Reads will not see the new key until the variable is unset or repointed.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
89
123
|
const argv = overrides.argv || [];
|
|
90
124
|
const flags = extractInitFlags(argv);
|
|
91
125
|
|
|
@@ -207,10 +207,12 @@ Otherwise, tell the operator what is happening in one short paragraph:
|
|
|
207
207
|
|
|
208
208
|
> Standing up your instance's own SOPS+age vault. This is what
|
|
209
209
|
> credentialed tools read their keys from. The vault lives in
|
|
210
|
-
> `.vault/`, the age private key lives
|
|
211
|
-
>
|
|
212
|
-
>
|
|
213
|
-
>
|
|
210
|
+
> `.vault/`, the age private key lives inside the instance at
|
|
211
|
+
> `.vault/age-key.txt` (never committed), and the encrypted store is
|
|
212
|
+
> safe to commit. Values you enter here never leave your laptop.
|
|
213
|
+
> HQ-mode instances keep a per-user key at
|
|
214
|
+
> `~/.config/stravica-pa/age-key.txt` instead (shared across the
|
|
215
|
+
> estate's canonical clones).
|
|
214
216
|
|
|
215
217
|
Ask, in one turn, for the values the enabled tools declare. For the
|
|
216
218
|
default atlassian install:
|
|
@@ -562,7 +562,7 @@ function validateLaunchdDaveRoot(value) {
|
|
|
562
562
|
+ '(DAVE_ROOT unset at install time, falling back to the installed '
|
|
563
563
|
+ 'package\'s own directory, which is version-pinned and silently breaks '
|
|
564
564
|
+ 'on the next upgrade or package rename). Set DAVE_ROOT explicitly in '
|
|
565
|
-
+ 'the shell running `
|
|
565
|
+
+ 'the shell running `pnpm exec scheduler install`, or set the schedule\'s '
|
|
566
566
|
+ 'environment.DAVE_ROOT to the real operator repo root.',
|
|
567
567
|
);
|
|
568
568
|
e.exitCode = 2;
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* list.mjs -- List
|
|
3
|
+
* list.mjs -- List scheduled tasks belonging to the invoking instance
|
|
4
4
|
*
|
|
5
5
|
* Usage: node node_modules/@stravica-ai/engineering-pa-core/src/scheduler/list.mjs [backend]
|
|
6
6
|
*
|
|
7
7
|
* Backends: cron, launchd, systemd, schtasks
|
|
8
8
|
* If no backend is specified, auto-detects and uses the first recommended.
|
|
9
|
+
*
|
|
10
|
+
* Agent-name resolution (0.1.52, w-2026-09-19-dave-001): the header,
|
|
11
|
+
* launchd label prefix, cron tag, systemd unit prefix, and schtasks
|
|
12
|
+
* name filter ALL derive from the invoking instance's `paName`
|
|
13
|
+
* (operator/config/preferences.json), or an explicit `--agent`
|
|
14
|
+
* override. There is no hard-coded fallback: a laptop that carries
|
|
15
|
+
* another estate's schedules would previously see them listed under
|
|
16
|
+
* this instance's `dave` default, and a subsequent `scheduler remove`
|
|
17
|
+
* would touch the wrong estate's plists. See ./resolve-agent.mjs for
|
|
18
|
+
* the resolution order and the escalation on failure.
|
|
9
19
|
*/
|
|
10
20
|
|
|
11
21
|
import { readdirSync, readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
@@ -15,54 +25,57 @@ import { execSync } from 'node:child_process';
|
|
|
15
25
|
import { fileURLToPath } from 'node:url';
|
|
16
26
|
import { detect } from './detect.mjs';
|
|
17
27
|
import { launchdLabelPrefix } from './install.mjs';
|
|
28
|
+
import { resolveAgent } from './resolve-agent.mjs';
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
|
-
* Render the leading name for the listing header. Uses the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Tag prefixes on the crontab / launchd / systemd / schtasks side are NOT
|
|
25
|
-
* renamed here \u2014 those are load-bearing for removal on existing installs
|
|
26
|
-
* (they are still `dave-` for backward compat).
|
|
31
|
+
* Render the leading name for the listing header. Uses the resolved
|
|
32
|
+
* agent name so both the header and the label prefix agree on which
|
|
33
|
+
* estate is being reported.
|
|
27
34
|
*
|
|
28
|
-
* @param {string}
|
|
35
|
+
* @param {string} agent
|
|
29
36
|
* @returns {string}
|
|
30
37
|
*/
|
|
31
38
|
function headerName(agent) {
|
|
32
|
-
|
|
33
|
-
return 'Schedules';
|
|
39
|
+
return agent;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
37
43
|
* List schedules for a given backend.
|
|
38
44
|
* @param {string} backend - Backend name (cron, launchd, systemd, schtasks)
|
|
39
45
|
* @param {object} [opts]
|
|
40
|
-
* @param {string} [opts.agent]
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
46
|
+
* @param {string} [opts.agent] Explicit agent override. Falls back to
|
|
47
|
+
* the invoking instance's `paName` (see ./resolve-agent.mjs).
|
|
48
|
+
* @param {string} [opts.repoRoot] Optional instance root override
|
|
49
|
+
* (tests inject; production callers omit this).
|
|
44
50
|
* @returns {string} Human-readable listing
|
|
45
51
|
*/
|
|
46
52
|
export function list(backend, opts = {}) {
|
|
47
|
-
|
|
53
|
+
// Validate the backend BEFORE resolving the agent name: an unknown
|
|
54
|
+
// backend is a usage error and needs to surface as "Unknown backend:
|
|
55
|
+
// <x>" regardless of whether the caller has a paName resolvable on
|
|
56
|
+
// this host. The prior ordering (agent first) turned a usage-shaped
|
|
57
|
+
// failure into a scheduler-config-shaped one, hiding the real cause.
|
|
58
|
+
if (backend !== 'cron' && backend !== 'launchd' && backend !== 'systemd' && backend !== 'schtasks') {
|
|
59
|
+
throw new Error(`Unknown backend: ${backend}`);
|
|
60
|
+
}
|
|
61
|
+
const { agent } = resolveAgent({ agent: opts.agent, repoRoot: opts.repoRoot });
|
|
48
62
|
switch (backend) {
|
|
49
63
|
case 'cron': return listCron(agent);
|
|
50
64
|
case 'launchd': return listLaunchd(agent);
|
|
51
65
|
case 'systemd': return listSystemd(agent);
|
|
52
66
|
case 'schtasks': return listSchtasks(agent);
|
|
53
|
-
default:
|
|
54
|
-
throw new Error(`Unknown backend: ${backend}`);
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
function listCron(agent) {
|
|
59
|
-
const lines = [`${headerName(agent)} schedules (cron):`, '
|
|
71
|
+
const lines = [`${headerName(agent)} schedules (cron):`, '─'.repeat(37)];
|
|
60
72
|
let found = false;
|
|
73
|
+
const tag = `# ${agent}-`;
|
|
61
74
|
|
|
62
75
|
try {
|
|
63
76
|
const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
64
77
|
for (const line of crontab.split('\n')) {
|
|
65
|
-
if (line.includes(
|
|
78
|
+
if (line.includes(tag)) {
|
|
66
79
|
lines.push(` ${line}`);
|
|
67
80
|
found = true;
|
|
68
81
|
}
|
|
@@ -76,7 +89,7 @@ function listCron(agent) {
|
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
function listLaunchd(agent) {
|
|
79
|
-
const lines = [`${headerName(agent)} schedules (launchd):`, '
|
|
92
|
+
const lines = [`${headerName(agent)} schedules (launchd):`, '─'.repeat(37)];
|
|
80
93
|
let found = false;
|
|
81
94
|
|
|
82
95
|
const prefix = launchdLabelPrefix(agent);
|
|
@@ -104,13 +117,14 @@ function listLaunchd(agent) {
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
function listSystemd(agent) {
|
|
107
|
-
const lines = [`${headerName(agent)} schedules (systemd):`, '
|
|
120
|
+
const lines = [`${headerName(agent)} schedules (systemd):`, '─'.repeat(37)];
|
|
108
121
|
let found = false;
|
|
122
|
+
const tag = `${agent}-`;
|
|
109
123
|
|
|
110
124
|
try {
|
|
111
125
|
const output = execSync('systemctl --user list-timers --no-pager', { encoding: 'utf8' });
|
|
112
126
|
for (const line of output.split('\n')) {
|
|
113
|
-
if (line.includes(
|
|
127
|
+
if (line.includes(tag)) {
|
|
114
128
|
lines.push(` ${line}`);
|
|
115
129
|
found = true;
|
|
116
130
|
}
|
|
@@ -124,13 +138,14 @@ function listSystemd(agent) {
|
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
function listSchtasks(agent) {
|
|
127
|
-
const lines = [`${headerName(agent)} schedules (schtasks):`, '
|
|
141
|
+
const lines = [`${headerName(agent)} schedules (schtasks):`, '─'.repeat(37)];
|
|
128
142
|
let found = false;
|
|
143
|
+
const tag = `${agent}-`;
|
|
129
144
|
|
|
130
145
|
try {
|
|
131
146
|
const output = execSync('schtasks.exe /Query /FO LIST', { encoding: 'utf8' });
|
|
132
147
|
for (const line of output.split('\n')) {
|
|
133
|
-
if (line.includes(
|
|
148
|
+
if (line.includes(tag)) {
|
|
134
149
|
lines.push(` ${line}`);
|
|
135
150
|
found = true;
|
|
136
151
|
}
|
|
@@ -184,6 +199,6 @@ if (isDirectlyInvoked()) {
|
|
|
184
199
|
console.log(list(backend));
|
|
185
200
|
} catch (err) {
|
|
186
201
|
console.error(`Error: ${err.message}`);
|
|
187
|
-
process.exit(1);
|
|
202
|
+
process.exit(err.exitCode || 1);
|
|
188
203
|
}
|
|
189
204
|
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* remove.mjs -- Remove a
|
|
3
|
+
* remove.mjs -- Remove a scheduled task belonging to the invoking instance
|
|
4
4
|
*
|
|
5
5
|
* Usage: node node_modules/@stravica-ai/engineering-pa-core/src/scheduler/remove.mjs <schedule-id> <backend> [--delete-file]
|
|
6
6
|
*
|
|
7
7
|
* Removes the scheduled task identified by <schedule-id> from the
|
|
8
8
|
* specified backend. Also removes the schedule JSON from schedules/
|
|
9
9
|
* if --delete-file is passed.
|
|
10
|
+
*
|
|
11
|
+
* Agent-name resolution (0.1.52, w-2026-09-19-dave-001): the launchd
|
|
12
|
+
* label, cron tag, systemd unit, and schtasks name ALL derive from
|
|
13
|
+
* the invoking instance's `paName` (operator/config/preferences.json)
|
|
14
|
+
* or an explicit `--agent` override. Pre-0.1.52 defaulted to the
|
|
15
|
+
* literal `"dave"`, which on a laptop carrying another estate's
|
|
16
|
+
* schedules would unload/delete THAT estate's plists. There is no
|
|
17
|
+
* hard-coded fallback now — refusal is the safe behaviour when
|
|
18
|
+
* neither source resolves a name. See ./resolve-agent.mjs.
|
|
10
19
|
*/
|
|
11
20
|
|
|
12
21
|
import { existsSync, unlinkSync, realpathSync } from 'node:fs';
|
|
@@ -16,19 +25,20 @@ import { execSync } from 'node:child_process';
|
|
|
16
25
|
import { fileURLToPath } from 'node:url';
|
|
17
26
|
import { launchdLabelPrefix } from './install.mjs';
|
|
18
27
|
import { findRepoRootSync } from '../instance-root/find.mjs';
|
|
28
|
+
import { resolveAgent } from './resolve-agent.mjs';
|
|
19
29
|
|
|
20
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
31
|
const __dirname = dirname(__filename);
|
|
22
32
|
|
|
23
33
|
// ── Backend removal functions ────────────────────────────────────────
|
|
24
34
|
|
|
25
|
-
function removeCron(schedId) {
|
|
26
|
-
const tag = `#
|
|
35
|
+
function removeCron(schedId, agent) {
|
|
36
|
+
const tag = `# ${agent}-${schedId}`;
|
|
27
37
|
let existing = '';
|
|
28
38
|
try {
|
|
29
39
|
existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
30
40
|
} catch {
|
|
31
|
-
console.log(`No cron entry found for
|
|
41
|
+
console.log(`No cron entry found for ${agent}-${schedId}`);
|
|
32
42
|
return;
|
|
33
43
|
}
|
|
34
44
|
|
|
@@ -36,7 +46,7 @@ function removeCron(schedId) {
|
|
|
36
46
|
const filtered = lines.filter(line => !line.includes(tag)).join('\n').replace(/^\n+|\n+$/g, '');
|
|
37
47
|
|
|
38
48
|
if (filtered === existing.replace(/^\n+|\n+$/g, '')) {
|
|
39
|
-
console.log(`No cron entry found for
|
|
49
|
+
console.log(`No cron entry found for ${agent}-${schedId}`);
|
|
40
50
|
return;
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -46,7 +56,7 @@ function removeCron(schedId) {
|
|
|
46
56
|
try { execSync('crontab -r', { stdio: 'pipe' }); } catch { /* already empty */ }
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
console.log(`Removed cron schedule:
|
|
59
|
+
console.log(`Removed cron schedule: ${agent}-${schedId}`);
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function removeLaunchd(schedId, agent) {
|
|
@@ -68,8 +78,8 @@ function removeLaunchd(schedId, agent) {
|
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
function removeSystemd(schedId) {
|
|
72
|
-
const unitName =
|
|
81
|
+
function removeSystemd(schedId, agent) {
|
|
82
|
+
const unitName = `${agent}-${schedId}`;
|
|
73
83
|
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
74
84
|
|
|
75
85
|
// Stop and disable
|
|
@@ -95,8 +105,8 @@ function removeSystemd(schedId) {
|
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
98
|
-
function removeSchtasks(schedId) {
|
|
99
|
-
const taskName =
|
|
108
|
+
function removeSchtasks(schedId, agent) {
|
|
109
|
+
const taskName = `${agent}-${schedId}`;
|
|
100
110
|
try {
|
|
101
111
|
execSync(`schtasks.exe /Delete /TN "${taskName}" /F`, { stdio: 'pipe' });
|
|
102
112
|
console.log(`Removed schtasks schedule: ${taskName}`);
|
|
@@ -108,30 +118,41 @@ function removeSchtasks(schedId) {
|
|
|
108
118
|
// ── Main ─────────────────────────────────────────────────────────────
|
|
109
119
|
|
|
110
120
|
/**
|
|
111
|
-
* Remove a
|
|
121
|
+
* Remove a schedule from the specified backend for the invoking
|
|
122
|
+
* instance (or an explicit `--agent` override).
|
|
112
123
|
*
|
|
113
124
|
* Same BUG-2-class fix as install(): this used to have no return
|
|
114
125
|
* statement, so the CLI wrapper's `result.ok` check was reading
|
|
115
126
|
* `undefined.ok`. Every path now returns a result object.
|
|
127
|
+
*
|
|
116
128
|
* @param {string} schedId - Schedule identifier
|
|
117
129
|
* @param {string} backend - Backend name (cron, launchd, systemd, schtasks)
|
|
118
130
|
* @param {object} [options]
|
|
119
131
|
* @param {boolean} [options.deleteFile=false] - Also delete the schedule JSON file
|
|
120
|
-
* @param {string} [options.agent] -
|
|
121
|
-
*
|
|
132
|
+
* @param {string} [options.agent] - Explicit agent override. Falls
|
|
133
|
+
* back to the invoking instance's `paName` from preferences.json.
|
|
134
|
+
* @param {string} [options.repoRoot] - Optional instance root override
|
|
135
|
+
* (tests inject; production callers omit this).
|
|
136
|
+
* @returns {{ ok: boolean, id: string, backend: string, agent: string }}
|
|
122
137
|
*/
|
|
123
138
|
export function remove(schedId, backend, options = {}) {
|
|
139
|
+
// See list.mjs for the same ordering rationale: backend validation
|
|
140
|
+
// must run BEFORE agent resolution so an unknown backend is a
|
|
141
|
+
// usage-shaped error, not a scheduler-config-shaped one.
|
|
142
|
+
if (backend !== 'cron' && backend !== 'launchd' && backend !== 'systemd' && backend !== 'schtasks') {
|
|
143
|
+
throw new Error(`Unknown backend: ${backend}. Supported: cron, launchd, systemd, schtasks`);
|
|
144
|
+
}
|
|
145
|
+
const { agent } = resolveAgent({ agent: options.agent, repoRoot: options.repoRoot });
|
|
146
|
+
|
|
124
147
|
switch (backend) {
|
|
125
|
-
case 'cron': removeCron(schedId); break;
|
|
126
|
-
case 'launchd': removeLaunchd(schedId,
|
|
127
|
-
case 'systemd': removeSystemd(schedId); break;
|
|
128
|
-
case 'schtasks': removeSchtasks(schedId); break;
|
|
129
|
-
default:
|
|
130
|
-
throw new Error(`Unknown backend: ${backend}. Supported: cron, launchd, systemd, schtasks`);
|
|
148
|
+
case 'cron': removeCron(schedId, agent); break;
|
|
149
|
+
case 'launchd': removeLaunchd(schedId, agent); break;
|
|
150
|
+
case 'systemd': removeSystemd(schedId, agent); break;
|
|
151
|
+
case 'schtasks': removeSchtasks(schedId, agent); break;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
if (options.deleteFile) {
|
|
134
|
-
const daveRoot = process.env.DAVE_ROOT || process.env.PA_ROOT || findRepoRootSync();
|
|
155
|
+
const daveRoot = process.env.DAVE_ROOT || process.env.PA_ROOT || options.repoRoot || findRepoRootSync();
|
|
135
156
|
const schedFile = join(daveRoot, 'schedules', `${schedId}.json`);
|
|
136
157
|
if (existsSync(schedFile)) {
|
|
137
158
|
unlinkSync(schedFile);
|
|
@@ -139,7 +160,7 @@ export function remove(schedId, backend, options = {}) {
|
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
return { ok: true, id: schedId, backend };
|
|
163
|
+
return { ok: true, id: schedId, backend, agent };
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
// ── CLI entry point ──────────────────────────────────────────────────
|
|
@@ -172,6 +193,6 @@ if (isDirectlyInvoked()) {
|
|
|
172
193
|
remove(positional[0], positional[1], { deleteFile });
|
|
173
194
|
} catch (err) {
|
|
174
195
|
console.error(`Error: ${err.message}`);
|
|
175
|
-
process.exit(1);
|
|
196
|
+
process.exit(err.exitCode || 1);
|
|
176
197
|
}
|
|
177
198
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// resolve-agent.mjs — shared agent-name resolution for list + remove.
|
|
2
|
+
//
|
|
3
|
+
// Pre-0.1.52, `scheduler list` and `scheduler remove` defaulted the
|
|
4
|
+
// launchd label prefix (and the cron / systemd / schtasks tag prefix)
|
|
5
|
+
// to the literal string `"dave"`. On a laptop that carried more than
|
|
6
|
+
// one estate — the exact shape a colleague-hosted "demo then delete"
|
|
7
|
+
// solo instance sits beside another estate — this made the tools list
|
|
8
|
+
// and touch schedules belonging to OTHER estates. `install` had never
|
|
9
|
+
// had that problem because it reads the label from the schedule file's
|
|
10
|
+
// own `agent` field. `list` / `remove` have no schedule file at hand,
|
|
11
|
+
// so this helper resolves the invoking instance's `paName` from
|
|
12
|
+
// `operator/config/preferences.json` — the same source `install` and
|
|
13
|
+
// the 0.1.50 workspace header already use.
|
|
14
|
+
//
|
|
15
|
+
// Resolution order:
|
|
16
|
+
// 1. explicit `--agent <name>` override (unchanged);
|
|
17
|
+
// 2. `paName` from the invoking instance's preferences (base +
|
|
18
|
+
// overlay via `readPreferencesSync`);
|
|
19
|
+
// 3. no fallback — refuse with a message naming the preferences key
|
|
20
|
+
// and the `--agent` escape hatch. There is deliberately NO hard-
|
|
21
|
+
// coded `dave` fallback: silently defaulting is what caused this
|
|
22
|
+
// bug in the first place.
|
|
23
|
+
|
|
24
|
+
import { readPreferencesSync } from '../preferences.mjs';
|
|
25
|
+
import { findRepoRootSync } from '../instance-root/find.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the agent name to use for launchd label prefixing / cron /
|
|
29
|
+
* systemd / schtasks tag filtering.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} [opts]
|
|
32
|
+
* @param {string} [opts.agent] Explicit override (from `--agent`).
|
|
33
|
+
* @param {string} [opts.repoRoot] Optional instance root override
|
|
34
|
+
* (tests inject; defaults to `findRepoRootSync()`).
|
|
35
|
+
* @returns {{ agent: string, source: 'explicit'|'preferences' }}
|
|
36
|
+
* @throws {Error} exitCode:2 when neither source resolves a value.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveAgent(opts = {}) {
|
|
39
|
+
if (typeof opts.agent === 'string' && opts.agent.length > 0) {
|
|
40
|
+
return { agent: opts.agent, source: 'explicit' };
|
|
41
|
+
}
|
|
42
|
+
const repoRoot = opts.repoRoot || findRepoRootSync();
|
|
43
|
+
const prefs = readPreferencesSync(repoRoot);
|
|
44
|
+
if (prefs && typeof prefs.paName === 'string' && prefs.paName.length > 0) {
|
|
45
|
+
return { agent: prefs.paName, source: 'preferences' };
|
|
46
|
+
}
|
|
47
|
+
const err = new Error(
|
|
48
|
+
'scheduler: cannot resolve agent name. Set `paName` in '
|
|
49
|
+
+ 'operator/config/preferences.json for this instance, or pass '
|
|
50
|
+
+ '`--agent <name>` explicitly. No hard-coded fallback is used '
|
|
51
|
+
+ '(0.1.52, w-2026-09-19-dave-001): a silent default would target '
|
|
52
|
+
+ 'schedules belonging to another estate.',
|
|
53
|
+
);
|
|
54
|
+
err.exitCode = 2;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
@@ -294,7 +294,12 @@ export function handleApi(req, res, url, repoRoot, deps = {}) {
|
|
|
294
294
|
}
|
|
295
295
|
if (backend) {
|
|
296
296
|
try {
|
|
297
|
-
|
|
297
|
+
// Thread the workspace's known instance root through so the
|
|
298
|
+
// agent-name resolver reads THIS instance's preferences.json
|
|
299
|
+
// (paName) rather than walking up from process.cwd(). See
|
|
300
|
+
// core/src/scheduler/resolve-agent.mjs (0.1.52,
|
|
301
|
+
// w-2026-09-19-dave-001).
|
|
302
|
+
liveStatus = String(scheduler.list(backend, { repoRoot })).trim();
|
|
298
303
|
} catch (err) {
|
|
299
304
|
schedulerError = 'scheduler list failed for backend "' + backend + '": ' + (err && err.message ? err.message : String(err));
|
|
300
305
|
}
|
package/docs/onboarding-solo.md
CHANGED
|
@@ -110,17 +110,44 @@ Your PA has the `rcf-lite-engineer` role baked in. It builds greenfield applicat
|
|
|
110
110
|
|
|
111
111
|
## Removing an instance
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Do everything below from inside the instance, in this order. Deleting the instance directory does not stop the workspace server, does not unload the launchd job (macOS) or cron entry (Linux) the scheduler registered, and does not remove the age key. A loaded schedule keeps firing against a path that no longer exists, its errors turn up in the system log with no easy trace back to the retired instance, and a running workspace server keeps its port bound after its own `node_modules` has been deleted.
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
The steps are ordered so that a stop or listing that fails does not orphan a running background process. If you already deleted the instance directory before reading this, jump to the fallbacks in step 1.
|
|
116
116
|
|
|
117
|
-
1.
|
|
117
|
+
1. Stop the workspace server.
|
|
118
118
|
```
|
|
119
|
-
pnpm exec pa
|
|
119
|
+
pnpm exec pa workspace stop
|
|
120
120
|
```
|
|
121
|
-
|
|
121
|
+
The stop command reads the pid and port from `.ws.pid` at the instance root, sends SIGTERM, and waits up to 5s before force-killing. Confirm with `pnpm exec pa workspace status`. If the instance directory is already gone (so `pnpm exec` is no longer runnable), find the process by port and kill it directly:
|
|
122
122
|
```
|
|
123
|
-
|
|
123
|
+
cat .ws.pid # if .ws.pid still exists: {"pid":<N>,"port":<P>}
|
|
124
|
+
lsof -i :<P> | grep LISTEN # macOS/Linux: process holding the port
|
|
125
|
+
pgrep -lf digital-operator-pa-workspace # match the server's spawn command
|
|
126
|
+
kill <pid> # SIGTERM; kill -9 <pid> if it refuses
|
|
124
127
|
```
|
|
128
|
+
The server responds to `GET /__api/identity` with `{"server":"digital-operator-pa-workspace","repoRoot":"...","pid":<N>,...}`, so `curl -s http://127.0.0.1:<port>/__api/identity` will identify a rogue workspace server (and name the instance directory it was started from) before you kill it.
|
|
125
129
|
|
|
126
|
-
|
|
130
|
+
2. List every schedule the instance installed. Since 0.1.52 (w-2026-09-19-dave-001) the tool filters by this instance's `paName` from `operator/config/preferences.json`, so it only ever reports schedules that belong here:
|
|
131
|
+
```
|
|
132
|
+
pnpm exec scheduler list
|
|
133
|
+
```
|
|
134
|
+
The correct bin is `scheduler` (not `pa scheduler`; there is no `pa scheduler` subcommand). If the tool refuses because it cannot resolve a `paName`, pass `--agent <name>` explicitly.
|
|
135
|
+
|
|
136
|
+
3. Remove every id the list reports. `--delete-file` also removes the schedule JSON:
|
|
137
|
+
```
|
|
138
|
+
pnpm exec scheduler remove <id> --delete-file
|
|
139
|
+
```
|
|
140
|
+
Repeat per id.
|
|
141
|
+
|
|
142
|
+
4. Confirm nothing this instance registered is still loaded. On macOS, `launchctl list` and `crontab -l` should carry no entries with the `com.<paName>.` prefix respectively `# <paName>-` tag:
|
|
143
|
+
```
|
|
144
|
+
launchctl list | grep "com.<paName>\." # macOS
|
|
145
|
+
crontab -l | grep "# <paName>-" # cron backend
|
|
146
|
+
systemctl --user list-timers | grep "<paName>-" # linux/systemd
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
5. Only now delete the instance directory:
|
|
150
|
+
```
|
|
151
|
+
rm -rf $HOME/<pa-name>-instance
|
|
152
|
+
```
|
|
153
|
+
The age private key lives inside the instance at `.vault/age-key.txt` and goes with the directory. If you want a portable copy of the secrets first, run `pnpm exec secrets backup` from inside the instance BEFORE step 5.
|
package/package.json
CHANGED