@stravica-ai/digital-operator-pa 0.1.51 → 0.1.53

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.
@@ -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 (!filters.all) {
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);
@@ -25,18 +25,27 @@ Usage:
25
25
  Install a schedule onto its declared backend
26
26
  scheduler list [--backend <name>] [--agent <name>]
27
27
  List installed schedules
28
- scheduler remove <id> [--backend N] [--agent <name>] [--delete-file]
29
- Remove an installed schedule
28
+ scheduler remove <id|com.<agent>.<id>> [--backend N] [--agent <name>] [--delete-file]
29
+ Remove an installed schedule. --backend is
30
+ optional: when omitted, the id is looked up in
31
+ schedules/<id>.json and the file's own backend
32
+ field wins (OS-native detect is the fallback).
33
+ The label form the list verb prints as a
34
+ secondary line is accepted here directly.
30
35
  scheduler verify <schedule.json> Verify a schedule fires correctly
31
36
  scheduler -h, --help Show this help
32
37
 
33
38
  Schedule files live at <repoRoot>/schedules/<id>.json. The canonical
34
39
  schema is bundled at @stravica-ai/engineering-pa-core/schema/schedule.json.
35
40
 
36
- --agent (launchd only) targets a specific agent's label prefix
37
- (com.<agent>.<id>). A schedule file's own top-level "agent" field drives
38
- install; list/remove take it explicitly since they don't read a schedule
39
- file. Defaults to "dave" everywhere — omitting it is unchanged behaviour.
41
+ --agent targets a specific agent's label / tag prefix. Install reads
42
+ the agent from the schedule file's own top-level "agent" field. Since
43
+ 0.1.52 (w-2026-09-19-dave-001), list/remove resolve the agent from the
44
+ invoking instance's "paName" in operator/config/preferences.json when
45
+ --agent is omitted, and REFUSE (exit 2) when neither source produces a
46
+ value — there is no hard-coded "dave" fallback. This is the fix for the
47
+ colleague-laptop bug where the tools would list and touch another
48
+ estate's schedules under a silent default.
40
49
 
41
50
  Host enforcement (install): a schedule's "host" field is matched against
42
51
  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
- const paths = overrides.paths || defaultPaths({ ...overrides, allowGitRootWithoutVault: true });
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
 
@@ -77,16 +77,25 @@ done and move on. On a mismatch, print both versions and the recovery line
77
77
 
78
78
  ### 3. profileIntake
79
79
 
80
- Ask the operator four questions in one turn:
80
+ Ask the operator four questions in one turn. Question 4 is optional
81
+ and explicitly skippable — a first-day operator often has no strong
82
+ answer and a forced answer here becomes a stale steering signal for
83
+ the whole life of the instance. "Skip" is the safe default when the
84
+ operator hesitates.
81
85
 
82
86
  1. Your name and how you prefer to be addressed?
83
87
  2. Your role (job title, or "solo", or a short description)?
84
88
  3. Communication style you want (formal, casual, direct, verbose)?
85
- 4. Any topic areas the PA should be strong on from day one?
89
+ 4. (Optional, skip freely) Anything you would like the PA to lean into
90
+ right away? A one-line hint about the kind of work you plan to
91
+ throw at it (writing, code, admin, research, planning). This tunes
92
+ nothing critical; the PA discovers your patterns as it goes.
86
93
 
87
- Take the answers and write them into `operator/profile/profile.md`, overwriting
88
- the placeholder. Keep it a short human-readable paragraph plus a bulleted list
89
- of preferences. Do not invent fields the operator did not name.
94
+ Take the answers and write them into `operator/profile/profile.md`,
95
+ overwriting the placeholder. Keep it a short human-readable paragraph
96
+ plus a bulleted list of preferences. If Q4 was skipped or the answer
97
+ is `skip` / empty, do not write a Q4 line at all. Do not invent fields
98
+ the operator did not name.
90
99
 
91
100
  ### 4. aliasOffer
92
101
 
@@ -189,6 +198,19 @@ and the operator gets:
189
198
  Then stop. Do not silently skip to `atlassianOffer`. The whole
190
199
  onboarding is blocked on this step.
191
200
 
201
+ **Hard stop between step 6 and step 7.** Do not deliver the step-7
202
+ vault pitch, do not describe the vault, do not ask the operator for
203
+ credential values, and do not print the `secrets init` invocation while
204
+ `librarianSchedule` is anything other than `done` in the state file.
205
+ Step 7 is only reached AFTER `librarianSchedule` transitions to `done`
206
+ and a completed run is recorded in `.cache/distill/logs/runs.jsonl`.
207
+ If the operator asks about the vault in the meantime, name the pending
208
+ check (`librarianSchedule` awaiting a proven run) and pick step 7 back
209
+ up once the librarian step lands. A pre-emptive step-7 pitch reads to
210
+ the operator as a step already begun, and the vault step's first-time
211
+ material is expensive to redo cleanly if it lands under the wrong
212
+ prerequisite.
213
+
192
214
  ### 7. vault (conditionally mandatory, no opt-out)
193
215
 
194
216
  Stand up the SOPS+age secrets vault when an enabled tool declares a
@@ -207,10 +229,12 @@ Otherwise, tell the operator what is happening in one short paragraph:
207
229
 
208
230
  > Standing up your instance's own SOPS+age vault. This is what
209
231
  > credentialed tools read their keys from. The vault lives in
210
- > `.vault/`, the age private key lives in
211
- > `~/.config/stravica-pa/age-key.txt` (never committed), and the
212
- > encrypted store is safe to commit. Values you enter here never leave
213
- > your laptop.
232
+ > `.vault/`, the age private key lives inside the instance at
233
+ > `.vault/age-key.txt` (never committed), and the encrypted store is
234
+ > safe to commit. Values you enter here never leave your laptop.
235
+ > HQ-mode instances keep a per-user key at
236
+ > `~/.config/stravica-pa/age-key.txt` instead (shared across the
237
+ > estate's canonical clones).
214
238
 
215
239
  Ask, in one turn, for the values the enabled tools declare. For the
216
240
  default atlassian install:
@@ -16,7 +16,7 @@ The role exists because workflow design keeps recurring as ad-hoc work done inli
16
16
  | Reports to | The dispatching PA |
17
17
  | Orchestrator mode | No -- one designer produces one ruled workflow design end to end |
18
18
  | Primary output | Versioned flowchart diagram + Rule of Two mapping + open-questions/rulings record, registered as a project doc |
19
- | Activation | 8-section briefing contract naming the workflow's purpose, actors, external stores, trust boundaries, cadence constraints, and the output and registration paths |
19
+ | Activation | Routing cues surfaced at the PA layer (see Activation); dispatched with the 8-section briefing contract |
20
20
  | Domain | Agentic pipelines, scheduled automation, triage and approval flows, cross-estate processes |
21
21
 
22
22
 
@@ -43,6 +43,32 @@ The mapping shape, one row per autonomous loop:
43
43
  |---|---|---|---|---|
44
44
  | `<loop name>` | yes / no / constrained: `<control>` | yes / no / constrained: `<control>` | yes / no / constrained: `<control>` | `<what the operator approves before the next lane runs>` |
45
45
 
46
+ **This mapping is not the autonomy envelope, and the two do not translate.** Estates already carry a per-task-type approval vocabulary (`pa autonomy`: `fullyApproved` / `checkBeforeCommitting` / `needsFullSupervision`), and under pressure that vocabulary wins by default because the platform supplied it first. They answer different questions. The autonomy envelope classifies task types by how much the operator trusts the agent to act unsupervised; the Rule of Two mapping classifies a loop's capabilities by what attacker-influenced content could do with them. An envelope state cannot express which of [A]/[B]/[C] a loop holds, so it cannot show whether the rule is satisfied; a mapping cannot express whether the operator wants sign-off on a task type. There is no crosswalk: `fullyApproved` is not `yes`, `needsFullSupervision` is not a gate, and a mapping delivered in envelope vocabulary is wrong in kind, not in wording -- it is redone in the table shape above, which is the only valid form of the mapping; prose restatements in any vocabulary are commentary, not the mapping. Where the two systems genuinely touch, state the relationship as a consequence, never a synonym: a loop whose mapping requires an operator gate will normally sit no higher than `checkBeforeCommitting` in the estate's envelope, and the diagram may note that beside the gate, clearly labelled as the envelope's word.
47
+
48
+
49
+ ## Required estate bindings
50
+
51
+ The role depends on two estate surfaces. Both are declared here so absence is a loud, checkable condition and never a silent change of deliverable. One is a hard dependency; the other splits into a half that ships in core and a half that is estate-optional with a stock-safe default.
52
+
53
+ **1. Graphic-design rendering lane -- hard dependency, checked before design work starts.** The brief must name the lane binding: engine, invocation form, and model pin. The designer's first act on activation is a binding check: confirm the named lane exists and answers (a version or no-op invocation is sufficient). If the brief names no lane, or the named lane does not answer, the designer returns immediately with a single line -- `BINDING-MISSING: graphic-design lane -- <what the brief named, what was checked, what was absent>` -- and produces no artefact. This is the only permitted return that replaces the six-line contract. Absence of the lane never changes the artefact type: no markdown diagram, no mermaid block, no knowledge-page substitute, no self-authored HTML on the orchestrating model. A wrong-shaped artefact reads as a delivered design and buries the estate defect; a loud stop gets the binding registered once, for every future dispatch. An unregistered binary on the machine (a `codex` on the PATH, say) is not a binding; a binding is named in the brief or in the estate's declared overlay, nowhere else.
54
+
55
+ **2. Review loop -- split, and the split is precise.** The doc-status half ships in core and works on a stock instance: `pa project doc add` registers the diagram, `pa project doc status <draft|review|approved>` tracks the loop. The role uses these unconditionally and may assume them. The comment-landing half -- a surface where the operator's rulings physically arrive against the artefact (a reviews inbox, an annotator) -- is not core. It is an estate extension and is never assumed. The ruling-capture channel is an overlay point with a stock-safe default:
56
+
57
+ | Overlay point | Core default | Estate example |
58
+ |---|---|---|
59
+ | Ruling-capture channel | Rulings arrive through the dispatching PA: the operator rules in conversation, the PA carries the numbered rulings into the revision dispatch, and they are staged verbatim per the operator review loop | An estate-bound reviews inbox and annotator on the workspace |
60
+
61
+ On a stock instance the full loop is: doc registered at `draft`, moved to `review` when the v1 URL goes to the operator, operator reads at the serving URL and rules through the PA, ruled revision ships at the same URL, doc moved to `approved`. Nothing in this role's process may require a surface the estate has not declared; where this definition and an estate's actual surfaces disagree, the designer treats the estate as authoritative and flags the gap in `HANDBACK:` rather than designing against the missing surface.
62
+
63
+
64
+ ## Design verification (the plan, not the page)
65
+
66
+ Render verification proves the diagram displays; it says nothing about whether the design is true. Before handover the designer verifies the plan itself, and states the outcome -- verification that happened but was not stated is treated as not having happened.
67
+
68
+ - **Grounding.** Every external fact the design leans on -- system states and status sets, permissions, endpoints, schedule mechanics, tool availability -- is verified against the live system where the estate's access allows, or explicitly marked `assumed` and carried as a numbered open question. An unverifiable fact never silently becomes a design premise.
69
+ - **Coverage.** Every autonomous loop drawn on the diagram has a mapping row and every mapping row corresponds to a drawn loop; every gate in the mapping is drawn and every drawn gate appears in the mapping. A diagram/mapping mismatch is a design defect, not a formatting nit.
70
+ - **Statement.** The plan summary carries one explicit verification line: what was checked, how, and what remains assumed.
71
+
46
72
 
47
73
  ## The diagram contract
48
74
 
@@ -85,7 +111,7 @@ Standing rules for the lane invocation, whatever engine the estate binds:
85
111
  ## Process obligations
86
112
 
87
113
  - A work item exists before design work starts; dispatches are logged per the estate's spawn discipline.
88
- - The design lands in a project (created via the estate's project tooling), with the diagram registered as a doc.
114
+ - **The design lands in a project, and the project bootstrap is owned, not assumed.** The brief names an existing `projectId` when the design belongs to work already underway. When it does not, the designer creates the project itself -- as its first act after the binding check -- via the estate's project tooling (`pa project create`, slug named for the workflow), and reports the created id on the `REGISTRATION:` line. The dispatching PA is never required to pre-create a project in order to dispatch this role: bootstrap cost on the dispatch path is routing friction, and routing friction is what sent the first proving run into the knowledge tree, which needed no bootstrap. The diagram is registered as a project doc immediately on first write (`pa project doc add`, status `draft`), not at handover.
89
115
  - Rulings and design-shaping decisions are staged to the knowledge pipeline in real time, not batched.
90
116
 
91
117
 
@@ -96,6 +122,7 @@ The definition is PA-agnostic and operator-agnostic. These bindings are supplied
96
122
  | Overlay point | Core default | Estate example |
97
123
  |---|---|---|
98
124
  | Graphic-design lane | The estate's designated engine, invoked as a CLI with the model pinned on the command line | HQ and WSD: Codex `gpt-6-astra` one-shot via `codex exec` (the judgement, content and design lane under the 2026-09-13 allocation) |
125
+ | Ruling-capture channel | Rulings arrive through the dispatching PA: the operator rules in conversation, the PA carries the numbered rulings into the revision dispatch, and they are staged verbatim per the operator review loop | An estate-bound reviews inbox and annotator on the workspace |
99
126
  | Editorial hygiene | Operator-profile-driven | No emojis, no em-dashes in persisted artefacts |
100
127
  | Diagram theme | Operator display preference; light when unstated | Hard-coded light theme |
101
128
  | Serving surface | The instance workspace at the configured `workspacePort` (`viewer.html?file=<path>` or direct path serving) | WSD workspace, repo-path serving on port 4222 |
@@ -121,7 +148,8 @@ Every designer dispatch returns:
121
148
  - `DIAGRAM:` file path, serving URL, version marker text
122
149
  - `MAPPING:` the [A]/[B]/[C] table per autonomous loop in the duty-1 vocabulary, with the gate placements
123
150
  - `QUESTIONS/RULINGS:` open questions (v1) or the rulings folded in (vN), with anything still provisional flagged
124
- - `VERIFICATION:` render evidence (console state, external-request count, elements confirmed)
151
+ - `VERIFICATION (design):` what was verified against live systems and how, the diagram/mapping coverage check, and what remains assumed (per Design verification)
152
+ - `VERIFICATION (render):` render evidence (console state, external-request count, elements confirmed)
125
153
  - `REGISTRATION:` project doc path and status
126
154
  - `HANDBACK:` what the parent does next -- rulings to obtain, spec to ship cross-estate, build items to open
127
155
 
@@ -132,7 +160,7 @@ The parent only sees the final message. The return contract is the deliverable.
132
160
 
133
161
  - **Designs; never implements the workflow in the same dispatch.** Build work is separate, briefed against the ruled design.
134
162
  - **Never weakens or removes a Rule of Two gate** on its own judgement; that is an operator ruling.
135
- - **No mutation of the systems the workflow will touch.** The design artefact, its registration and the staged rulings are the only writes.
163
+ - **No mutation of the systems the workflow will touch.** The design artefact, the project that carries it (creation and doc registration via the estate's project tooling, when the brief names no existing project), and the staged rulings are the only writes.
136
164
  - **Depth-2 posture: the designer spawns no agents.** The graphic-design lane is a tool call from the designer's own session; an agent-role lane is a sibling dispatch run by the PA.
137
165
 
138
166
 
@@ -164,10 +192,29 @@ Stop and escalate if:
164
192
 
165
193
  - The workflow cannot be decomposed to satisfy the Rule of Two without destroying its purpose; the operator decides the risk posture.
166
194
  - An operator instruction appears to conflict with the Rule of Two; surface the conflict with the mapping, do not silently comply or silently refuse.
167
- - The graphic-design lane is unavailable or fails within the bounded iterations.
195
+ - The graphic-design lane is unavailable at the activation binding check, or fails within the bounded iterations.
168
196
  - The brief lacks the actors, external stores, or trust boundaries needed to produce the [A]/[B]/[C] mapping; do not guess a security architecture.
169
197
 
170
198
 
171
199
  ## Activation
172
200
 
173
- The Agentic Workflow Designer activates when the PA delegates a workflow design with a briefing contract. The brief names the workflow's purpose, actors, external stores, trust boundaries, cadence constraints, the project and output paths, the estate's lane binding and model pin, and the designer's own model pin, and it carries the standard return-timing clause (return on completion or abort only). No external dependencies beyond the estate's lane tooling, a headless browser for render verification, and the project tooling for registration.
201
+ ### Routing cues (for the dispatching PA -- lift this block verbatim)
202
+
203
+ This block is written in the dispatcher's vocabulary, not the designer's, and it is designed to be composed into the PA's own operating surface by the platform. A cue that lives only in a role file the PA never opens routes nothing; the role declares the cues here, and the estate is responsible for surfacing them where routing decisions are made. The recognition test is deliberately one question, because a test that costs more thought than answering inline will lose to answering inline every time.
204
+
205
+ **The test: would a correct answer contain a flow -- more than one actor, or at least one decision or approval point?** If yes, dispatch this role. The PA does not sketch the flow inline first; an inline sketch is the deliverable done badly, and it anchors the operator on an unruled, unmapped design.
206
+
207
+ Operator phrasings that match, however small the ask sounds:
208
+
209
+ - "design a process / workflow / pipeline / automation" for anything
210
+ - anything involving status transitions, approval steps, handoffs, triage rules, escalation paths, or "who does what when"
211
+ - any automation that will run unattended: scheduled, triggered, or looping
212
+ - a correction of a prose answer toward an artefact ("as a designed workflow", "produce the real artefact", "not prose") -- a second ask in this shape after an inline answer means the first ask was a missed dispatch, not that a new kind of request has arrived
213
+
214
+ Non-cues (answer inline): a status question about an existing workflow; a factual question about this role or the Rule of Two; a prose edit to a ruled diagram's supporting text that changes no lane, gate, or mapping.
215
+
216
+ **A workflow design is project work, never knowledge capture.** Writing the design into the knowledge tree (`knowledge/`, `operator/knowledge/`) is a routing failure even when the content is good: knowledge records what has been ruled; the project carries what is being designed and reviewed. The knowledge tree is also the cheaper path on a fresh instance precisely because it needs no bootstrap, which is why this rule is stated here and not left to taste.
217
+
218
+ ### Activation brief (what arrives)
219
+
220
+ The Agentic Workflow Designer activates when the PA delegates a workflow design with a briefing contract. The brief names the workflow's purpose, actors, external stores, trust boundaries, cadence constraints, the project id where one already exists (see Process obligations for bootstrap when none does), the output paths, the estate's lane binding and model pin, and the designer's own model pin, and it carries the standard return-timing clause (return on completion or abort only). No external dependencies beyond the estate's lane tooling, a headless browser for render verification, and the project tooling for registration and bootstrap.
@@ -25,30 +25,58 @@ import { findRepoRootSync } from '../instance-root/find.mjs';
25
25
  const __filename = fileURLToPath(import.meta.url);
26
26
  const __dirname = dirname(__filename);
27
27
 
28
- /** Default agent id used when a schedule does not declare one. Backward-compat anchor. */
29
- export const DEFAULT_AGENT = 'dave';
28
+ /**
29
+ * Return the schedule's `agent` field, refusing when it is missing.
30
+ * 0.1.53 item E: the scheduler no longer carries a literal-`dave`
31
+ * default. Every schedule file has always declared its own `agent`
32
+ * field (the scaffold's `buildDistillationScheduleJson` sets it, the
33
+ * shared JSON schema documents it, and 0.1.52's `list`/`remove` do
34
+ * not silently default it either). A file without an `agent` field
35
+ * is a schema break, and refusing here is safer than silently
36
+ * spraying `dave-<id>` tags across whatever estate this laptop
37
+ * carries.
38
+ * @param {object} sched
39
+ * @returns {string}
40
+ * @throws {Error & { exitCode: 2 }}
41
+ */
42
+ export function schedAgent(sched) {
43
+ if (sched && typeof sched.agent === 'string' && sched.agent.length > 0) {
44
+ return sched.agent;
45
+ }
46
+ const err = new Error(
47
+ `scheduler: schedule file is missing the top-level "agent" field. Add `
48
+ + '"agent": "<paName>" alongside "id" (matches operator/config/preferences.json:paName). '
49
+ + 'The pre-0.1.53 default `"dave"` has been removed to keep cross-estate laptops safe.',
50
+ );
51
+ err.exitCode = 2;
52
+ throw err;
53
+ }
30
54
 
31
55
  /**
32
56
  * Compute the launchd label prefix for an agent. Centralised so install,
33
- * remove, and list all agree on the naming scheme. Defaults to the
34
- * pre-existing `com.dave.` behaviour when no agent is given.
35
- * @param {string|undefined} agent
57
+ * remove, and list all agree on the naming scheme. The agent name comes
58
+ * from the schedule file (install path) or the invoking instance's
59
+ * `paName` (list/remove path); there is no hard-coded default.
60
+ * @param {string} agent
36
61
  * @returns {string}
37
62
  */
38
63
  export function launchdLabelPrefix(agent) {
39
- return `com.${agent || DEFAULT_AGENT}.`;
64
+ if (!agent) {
65
+ const err = new Error('launchdLabelPrefix: agent is required (no default).');
66
+ err.exitCode = 2;
67
+ throw err;
68
+ }
69
+ return `com.${agent}.`;
40
70
  }
41
71
 
42
72
  /**
43
73
  * Resolve the full launchd label for a schedule. Agent-derived: reads
44
- * `sched.agent` (optional field on the schedule JSON), falling back to
45
- * DEFAULT_AGENT so existing schedule files with no `agent` field keep
46
- * producing exactly `com.dave.<id>`, unchanged from pre-feature behaviour.
74
+ * `sched.agent` and refuses when it is absent (see schedAgent).
47
75
  * @param {object} sched
48
76
  * @returns {string}
49
77
  */
50
78
  export function resolveLaunchdLabel(sched) {
51
- return `${launchdLabelPrefix(sched.agent)}${sched.id}`;
79
+ return `${launchdLabelPrefix(schedAgent(sched))}${sched.id}`;
52
80
  }
53
81
 
54
82
  /**
@@ -81,12 +109,31 @@ function readSchedule(filePath) {
81
109
  }
82
110
 
83
111
  /**
84
- * Compute the resolved DAVE_ROOT for the current install.
112
+ * Compute the resolved PA repo root for the current install. 0.1.53
113
+ * item E: local resolution first, PA_ROOT as an explicit override
114
+ * (never leading), DAVE_ROOT accepted as a deprecated alias with a
115
+ * one-line notice.
85
116
  * @returns {string}
86
117
  */
87
118
  function computeDaveRoot() {
88
- return process.env.DAVE_ROOT || process.env.PA_ROOT || findRepoRootSync();
119
+ const local = findRepoRootSync();
120
+ if (process.env.PA_ROOT) return process.env.PA_ROOT;
121
+ if (process.env.DAVE_ROOT) {
122
+ // Emit the notice at most once per process to keep install output
123
+ // tidy; the flag lives on the resolver so tests can reset it.
124
+ if (!computeDaveRoot._noticed) {
125
+ process.stderr.write(
126
+ 'Notice: DAVE_ROOT is a deprecated alias for PA_ROOT. Set PA_ROOT '
127
+ + 'instead; DAVE_ROOT support will be removed in a future release.\n',
128
+ );
129
+ computeDaveRoot._noticed = true;
130
+ }
131
+ return process.env.DAVE_ROOT;
132
+ }
133
+ return local;
89
134
  }
135
+ // Reset hook for tests.
136
+ computeDaveRoot._noticed = false;
90
137
 
91
138
  /**
92
139
  * Resolve the DAVE_ROOT variable and working directory.
@@ -271,9 +318,10 @@ export function checkNameCollision(scheduleFile, sched, opts = {}) {
271
318
  continue;
272
319
  }
273
320
  if (other && other.id === sched.id) {
321
+ const agent = schedAgent(sched);
274
322
  const e = new Error(
275
323
  `schedule id "${sched.id}" collides with ${candidate} — two different schedule files `
276
- + `resolve to the same task id/tag ("dave-${sched.id}"). Rename one of the ids before installing.`,
324
+ + `resolve to the same task id/tag ("${agent}-${sched.id}"). Rename one of the ids before installing.`,
277
325
  );
278
326
  e.exitCode = 2;
279
327
  throw e;
@@ -312,13 +360,12 @@ export function detectNvm(homeDir = homedir()) {
312
360
 
313
361
  /**
314
362
  * Default per-run log path for a schedule's cron entry, matching the
315
- * real house pattern (`/tmp/dave-knowledge-eval-daily.log` for
316
- * agent=dave, id=knowledge-eval-daily).
363
+ * real house pattern (`/tmp/<agent>-<id>.log`).
317
364
  * @param {object} sched
318
365
  * @returns {string}
319
366
  */
320
367
  export function defaultCronLogPath(sched) {
321
- return `/tmp/${sched.agent || DEFAULT_AGENT}-${sched.id}.log`;
368
+ return `/tmp/${schedAgent(sched)}-${sched.id}.log`;
322
369
  }
323
370
 
324
371
  /**
@@ -416,14 +463,15 @@ export function buildCronCommand(sched, workDir, env, opts = {}) {
416
463
  * @returns {string}
417
464
  */
418
465
  export function buildCronLine(sched, workDir, env, opts = {}) {
419
- const tag = `dave-${sched.id}`;
466
+ const tag = `${schedAgent(sched)}-${sched.id}`;
420
467
  const { wrapped } = buildCronCommand(sched, workDir, env, opts);
421
468
  return `${sched.schedule} ${wrapped} # ${tag}`;
422
469
  }
423
470
 
424
471
  function installCron(sched, workDir) {
472
+ const agent = schedAgent(sched);
425
473
  const env = resolveEnvironment(sched);
426
- const tag = `# dave-${sched.id}`;
474
+ const tag = `# ${agent}-${sched.id}`;
427
475
  const hasNvm = detectNvm();
428
476
  const entry = buildCronLine(sched, workDir, env, { hasNvm });
429
477
 
@@ -444,10 +492,10 @@ function installCron(sched, workDir) {
444
492
  const newCrontab = filtered ? `${filtered}\n${entry}\n` : `${entry}\n`;
445
493
  execSync('crontab -', { input: newCrontab, encoding: 'utf8' });
446
494
 
447
- console.log(`Installed cron schedule: dave-${sched.id}`);
495
+ console.log(`Installed cron schedule: ${agent}-${sched.id}`);
448
496
  console.log(` ${sched.schedule} -- ${sched.description || ''}`);
449
497
 
450
- return { tag: `dave-${sched.id}` };
498
+ return { tag: `${agent}-${sched.id}` };
451
499
  }
452
500
 
453
501
  // ── Backend: launchd ─────────────────────────────────────────────────
@@ -562,7 +610,7 @@ function validateLaunchdDaveRoot(value) {
562
610
  + '(DAVE_ROOT unset at install time, falling back to the installed '
563
611
  + 'package\'s own directory, which is version-pinned and silently breaks '
564
612
  + 'on the next upgrade or package rename). Set DAVE_ROOT explicitly in '
565
- + 'the shell running `pa scheduler install`, or set the schedule\'s '
613
+ + 'the shell running `pnpm exec scheduler install`, or set the schedule\'s '
566
614
  + 'environment.DAVE_ROOT to the real operator repo root.',
567
615
  );
568
616
  e.exitCode = 2;
@@ -696,7 +744,7 @@ export function buildStartCalendarIntervalBlock(cron) {
696
744
  * Generate the full plist content for a schedule. Pure function — safe to
697
745
  * call in tests. Always emits an EnvironmentVariables block on launchd
698
746
  * (auto-injected defaults when the schedule has no explicit environment).
699
- * The launchd Label is agent-derived (`com.<sched.agent||'dave'>.<id>`) —
747
+ * The launchd Label is agent-derived (`com.<sched.agent>.<id>`) —
700
748
  * see resolveLaunchdLabel.
701
749
  * @param {object} sched
702
750
  * @param {string} workDir
@@ -736,9 +784,9 @@ ${programArgs}
736
784
  <key>WorkingDirectory</key>
737
785
  <string>${xmlEscape(workDir)}</string>
738
786
  ${envBlock}${calendarBlock} <key>StandardOutPath</key>
739
- <string>/tmp/dave-${sched.id}-stdout.log</string>
787
+ <string>/tmp/${schedAgent(sched)}-${sched.id}-stdout.log</string>
740
788
  <key>StandardErrorPath</key>
741
- <string>/tmp/dave-${sched.id}-stderr.log</string>
789
+ <string>/tmp/${schedAgent(sched)}-${sched.id}-stderr.log</string>
742
790
  <key>RunAtLoad</key>
743
791
  <false/>
744
792
  </dict>
@@ -820,7 +868,7 @@ WantedBy=timers.target
820
868
  }
821
869
 
822
870
  function installSystemd(sched, workDir, cron) {
823
- const unitName = `dave-${sched.id}`;
871
+ const unitName = `${schedAgent(sched)}-${sched.id}`;
824
872
  const unitDir = join(homedir(), '.config', 'systemd', 'user');
825
873
  mkdirSync(unitDir, { recursive: true });
826
874
 
@@ -868,7 +916,7 @@ export function buildSchtasksCommand(sched, workDir, env) {
868
916
  }
869
917
 
870
918
  function installSchtasks(sched, workDir, cron) {
871
- const taskName = `dave-${sched.id}`;
919
+ const taskName = `${schedAgent(sched)}-${sched.id}`;
872
920
  const env = resolveEnvironment(sched);
873
921
 
874
922
  const fmtHour = cron.hour === '*' ? '00' : cron.hour.padStart(2, '0');