@stravica-ai/digital-operator-pa 0.1.35 → 0.1.37
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 +9 -6
- package/dist/engineering-pa-cli/src/secrets/vault.mjs +198 -15
- package/dist/engineering-pa-core/docs/tools/slice.md +30 -9
- package/dist/engineering-pa-core/src/slice/cli.mjs +12 -0
- package/dist/engineering-pa-core/src/slice/prompts/dave-boot.md +35 -33
- package/dist/engineering-pa-core/src/slice/run.mjs +142 -4
- package/dist/engineering-pa-library-cli/src/tier-map.mjs +101 -0
- package/dist/engineering-pa-library-cli/src/verbs/put.mjs +23 -2
- package/dist/engineering-pa-workspace/src/api.mjs +41 -30
- package/dist/engineering-pa-workspace/src/guards.mjs +161 -0
- package/dist/engineering-pa-workspace/src/server.mjs +109 -4
- package/dist/engineering-pa-workspace/src/ws.mjs +20 -8
- package/package.json +1 -1
|
@@ -326,12 +326,15 @@ Exit codes (tooling standard §6):
|
|
|
326
326
|
const WORKSPACE_USAGE = `pa workspace — workspace server lifecycle
|
|
327
327
|
|
|
328
328
|
Usage:
|
|
329
|
-
pa workspace start [--port N] [--no-open] Start the workspace server
|
|
330
|
-
pa workspace stop
|
|
331
|
-
pa workspace restart [--port N] [--no-open] Restart (supervisor-aware: launchctl kickstart / systemctl restart)
|
|
332
|
-
pa workspace status
|
|
333
|
-
pa workspace open
|
|
334
|
-
pa workspace -h, --help
|
|
329
|
+
pa workspace start [--port N] [--host H] [--no-open] Start the workspace server
|
|
330
|
+
pa workspace stop Stop the running server (refuses if supervised)
|
|
331
|
+
pa workspace restart [--port N] [--host H] [--no-open] Restart (supervisor-aware: launchctl kickstart / systemctl restart)
|
|
332
|
+
pa workspace status Show server status (supervisor-aware)
|
|
333
|
+
pa workspace open Open the workspace URL in a browser
|
|
334
|
+
pa workspace -h, --help Show this help
|
|
335
|
+
|
|
336
|
+
Bind host defaults to loopback (127.0.0.1). Use --host <addr> (or the
|
|
337
|
+
WORKSPACE_HOST env var) to expose the workspace on another interface.
|
|
335
338
|
|
|
336
339
|
When the workspace is supervised by launchd (~/Library/LaunchAgents/com.dave.workspace.plist)
|
|
337
340
|
or systemd (~/.config/systemd/user/dave-workspace.service), \`stop\` is refused and \`restart\`
|
|
@@ -790,6 +790,16 @@ export async function sopsDecrypt(paths, filePath) {
|
|
|
790
790
|
* scoped-vault fixtures use). Order is stable, duplicates deduplicated.
|
|
791
791
|
* Returns an empty array on any read failure. This helper only feeds a
|
|
792
792
|
* diagnostic message and must never throw over its own IO.
|
|
793
|
+
*
|
|
794
|
+
* NOTE: this reads the UNION of recipients across every creation_rule
|
|
795
|
+
* in the file. Scoped `.sops.yaml` configs typically declare one rule
|
|
796
|
+
* per encrypted file, and a caller who is a recipient in ONE scope but
|
|
797
|
+
* not another still matches this union. That is why the false-absence
|
|
798
|
+
* diagnostic surfaced on ops-01 (w-2026-09-03-dave-011): the caller's
|
|
799
|
+
* pubkey belonged to the seo-ops rule, not the hq-estate rule, but the
|
|
800
|
+
* union read showed it. Use `recipientsForTargetFile` to get the list
|
|
801
|
+
* for a specific file instead; the union stays here as a strict
|
|
802
|
+
* fallback for configs the narrow parser cannot recognise.
|
|
793
803
|
*/
|
|
794
804
|
export function readRecipientsFromSopsConfig(sopsConfigPath) {
|
|
795
805
|
let text;
|
|
@@ -812,31 +822,204 @@ export function readRecipientsFromSopsConfig(sopsConfigPath) {
|
|
|
812
822
|
return out;
|
|
813
823
|
}
|
|
814
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Parse creation_rules from a sops config file and return the recipient
|
|
827
|
+
* age pubkeys for the first rule whose `path_regex` matches the given
|
|
828
|
+
* encrypted file's basename. Returns null when the parser cannot pick a
|
|
829
|
+
* matching rule (caller falls back to the union read). Returns an empty
|
|
830
|
+
* array for a matching rule whose `age:` field is empty.
|
|
831
|
+
*
|
|
832
|
+
* The parser is deliberately narrow: it splits into blocks starting at
|
|
833
|
+
* `- path_regex:` lines and reads two fields per block, `path_regex`
|
|
834
|
+
* and `age`. Both inline (`age: "age1a,age1b"`) and folded multi-line
|
|
835
|
+
* (`age: >-\n age1a\n age1b`) shapes are handled: the two forms the
|
|
836
|
+
* estate's provisioning tooling emits. YAML this parser cannot
|
|
837
|
+
* recognise falls back to the union at the caller, so a partial config
|
|
838
|
+
* still produces a truthful message rather than a silent absence claim.
|
|
839
|
+
*
|
|
840
|
+
* Root-cause cure for w-2026-09-03-dave-011: `readRecipientsFromSopsConfig`
|
|
841
|
+
* unioned pubkeys across every creation_rule, so on ops-01 the caller
|
|
842
|
+
* key (recipient of seo-ops, not hq-estate) appeared in the printed
|
|
843
|
+
* "recipients" list, contradicting the "not in the recipient list"
|
|
844
|
+
* assertion. Scoping the recipient list to the actual target file's
|
|
845
|
+
* matching rule keeps the wording honest.
|
|
846
|
+
*/
|
|
847
|
+
export function recipientsForTargetFile(sopsConfigPath, targetFile) {
|
|
848
|
+
let text;
|
|
849
|
+
try {
|
|
850
|
+
text = readFileSync(sopsConfigPath, 'utf-8');
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
if (!targetFile) return null;
|
|
855
|
+
const targetName = basename(targetFile);
|
|
856
|
+
const lines = text.split('\n');
|
|
857
|
+
const rules = [];
|
|
858
|
+
let current = null;
|
|
859
|
+
let foldingAge = false;
|
|
860
|
+
const ruleStartRe = /^\s*-\s*path_regex:\s*(.+?)\s*$/;
|
|
861
|
+
const inlineAgeRe = /^\s*age:\s*"?([^"]*?)"?\s*$/;
|
|
862
|
+
const foldedAgeStartRe = /^\s*age:\s*>-?\s*$/;
|
|
863
|
+
const ageInlineExtract = /age1[0-9a-z]+/g;
|
|
864
|
+
|
|
865
|
+
for (const line of lines) {
|
|
866
|
+
const ruleStart = ruleStartRe.exec(line);
|
|
867
|
+
if (ruleStart) {
|
|
868
|
+
if (current) rules.push(current);
|
|
869
|
+
current = { pathRegex: ruleStart[1], age: [] };
|
|
870
|
+
foldingAge = false;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (!current) continue;
|
|
874
|
+
if (foldingAge) {
|
|
875
|
+
// Folded block ends at a blank line, a new key at the block's
|
|
876
|
+
// indent, or a new list item. Anything else is a wrapped value.
|
|
877
|
+
const trimmed = line.trim();
|
|
878
|
+
if (trimmed === '' || /^[A-Za-z_-]+\s*:/.test(trimmed) || /^-\s/.test(trimmed)) {
|
|
879
|
+
foldingAge = false;
|
|
880
|
+
// fall through so this line can still start a new rule below
|
|
881
|
+
} else {
|
|
882
|
+
for (const match of trimmed.matchAll(ageInlineExtract)) current.age.push(match[0]);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (foldedAgeStartRe.test(line)) {
|
|
887
|
+
foldingAge = true;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const inline = inlineAgeRe.exec(line);
|
|
891
|
+
if (inline) {
|
|
892
|
+
for (const match of inline[1].matchAll(ageInlineExtract)) current.age.push(match[0]);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (current) rules.push(current);
|
|
896
|
+
|
|
897
|
+
for (const rule of rules) {
|
|
898
|
+
let re;
|
|
899
|
+
try { re = new RegExp(rule.pathRegex); } catch { continue; }
|
|
900
|
+
if (re.test(targetName)) {
|
|
901
|
+
const seen = new Set();
|
|
902
|
+
const out = [];
|
|
903
|
+
for (const k of rule.age) if (!seen.has(k)) { seen.add(k); out.push(k); }
|
|
904
|
+
return out;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Read every `# public key: age1...` line from an age key file. A key
|
|
912
|
+
* file with a single identity yields one entry; a file with multiple
|
|
913
|
+
* identities yields one entry per identity, in file order (the order
|
|
914
|
+
* sops tries them when SOPS_AGE_KEY_FILE points at the file). Returns
|
|
915
|
+
* an empty array on any read failure or when no comment can be parsed.
|
|
916
|
+
* Only feeds diagnostic output: never throws over its own IO.
|
|
917
|
+
*/
|
|
918
|
+
export function publicKeysFromKeyFile(keyFile) {
|
|
919
|
+
let contents;
|
|
920
|
+
try {
|
|
921
|
+
contents = readFileSync(keyFile, 'utf-8');
|
|
922
|
+
} catch {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
const out = [];
|
|
926
|
+
const seen = new Set();
|
|
927
|
+
const re = /public key:\s*(age1[0-9a-z]+)/gi;
|
|
928
|
+
let m;
|
|
929
|
+
while ((m = re.exec(contents)) !== null) {
|
|
930
|
+
if (!seen.has(m[1])) {
|
|
931
|
+
seen.add(m[1]);
|
|
932
|
+
out.push(m[1]);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return out;
|
|
936
|
+
}
|
|
937
|
+
|
|
815
938
|
/**
|
|
816
939
|
* Build the single stderr diagnostic line that names, without ever
|
|
817
940
|
* decrypting anything, WHY a decrypt failed: the scope (or vault path),
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
941
|
+
* every candidate age public key the caller's key file contains (the
|
|
942
|
+
* exact set sops tries when SOPS_AGE_KEY_FILE points at that file, in
|
|
943
|
+
* file order), the target file's recipient list (scoped to the rule
|
|
944
|
+
* whose path_regex matches, when the parser can pin one, otherwise the
|
|
945
|
+
* union of all rules), and a truthful "is / is not in the list" verdict.
|
|
946
|
+
*
|
|
947
|
+
* Cures two live defects (w-2026-09-03-dave-011):
|
|
948
|
+
* 1. A file with multiple identities used to report only the first
|
|
949
|
+
* `public key:` line. Now every identity in the key file is named.
|
|
950
|
+
* 2. The wording used to assert absence unconditionally, printing a
|
|
951
|
+
* recipient list drawn from every creation_rule in the sops config.
|
|
952
|
+
* When the caller's key was a recipient of ANOTHER rule the printed
|
|
953
|
+
* list contradicted the assertion. Now the recipients are scoped
|
|
954
|
+
* to the target's own rule, and the message states membership
|
|
955
|
+
* truthfully: never claims absence when the list contains the key.
|
|
823
956
|
*/
|
|
824
957
|
export function formatDecryptFailure(paths) {
|
|
825
958
|
const target = paths.scope
|
|
826
959
|
? `scope "${paths.scope}"`
|
|
827
960
|
: (paths.secretsFile ? `vault ${paths.secretsFile}` : 'vault');
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
961
|
+
|
|
962
|
+
const callerPubs = paths.keyFile ? publicKeysFromKeyFile(paths.keyFile) : [];
|
|
963
|
+
|
|
964
|
+
// Prefer the recipient list of the specific target file's rule. Fall
|
|
965
|
+
// back to the union of every rule when the narrow parser cannot pin
|
|
966
|
+
// one; the wording checks membership either way, so falling back does
|
|
967
|
+
// not reintroduce the false-absence class the union alone caused.
|
|
968
|
+
let scopedRecipients = null;
|
|
969
|
+
if (paths.sopsConfig && paths.secretsFile) {
|
|
970
|
+
scopedRecipients = recipientsForTargetFile(paths.sopsConfig, paths.secretsFile);
|
|
971
|
+
}
|
|
972
|
+
const unionRecipients = paths.sopsConfig ? readRecipientsFromSopsConfig(paths.sopsConfig) : [];
|
|
973
|
+
const recipients = scopedRecipients ?? unionRecipients;
|
|
974
|
+
const recipientSet = new Set(recipients);
|
|
975
|
+
const matched = callerPubs.filter((k) => recipientSet.has(k));
|
|
976
|
+
|
|
977
|
+
const keyFileDesc = paths.keyFile ? ` (from ${paths.keyFile})` : '';
|
|
978
|
+
let callerDesc;
|
|
979
|
+
if (callerPubs.length === 0) {
|
|
980
|
+
callerDesc = paths.keyFile
|
|
981
|
+
? `no age public key could be parsed from ${paths.keyFile}`
|
|
982
|
+
: 'no age key file path known';
|
|
983
|
+
} else if (callerPubs.length === 1) {
|
|
984
|
+
callerDesc = `age key ${callerPubs[0]}${keyFileDesc}`;
|
|
985
|
+
} else {
|
|
986
|
+
callerDesc = `age keys ${callerPubs.join(', ')}${keyFileDesc}`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const recipientsLabel = scopedRecipients && paths.secretsFile
|
|
990
|
+
? `recipients for ${basename(paths.secretsFile)} (from ${paths.sopsConfig})`
|
|
991
|
+
: `recipients (from ${paths.sopsConfig ?? '<no sops config>'})`;
|
|
836
992
|
const recipientsDesc = recipients.length > 0
|
|
837
|
-
?
|
|
993
|
+
? `${recipientsLabel}: ${recipients.join(', ')}`
|
|
838
994
|
: `recipients could not be read from ${paths.sopsConfig ?? '<no sops config>'}`;
|
|
839
|
-
|
|
995
|
+
|
|
996
|
+
if (matched.length > 0) {
|
|
997
|
+
// Truthful branch: sops still refused even though the caller keys
|
|
998
|
+
// ARE listed as intended recipients. The two live causes are stale
|
|
999
|
+
// recipient stanzas in the encrypted store (needs `sops updatekeys`)
|
|
1000
|
+
// and unreadable key file permissions.
|
|
1001
|
+
const matchDesc = matched.length === 1
|
|
1002
|
+
? `${matched[0]} IS listed as a recipient`
|
|
1003
|
+
: `${matched.join(', ')} ARE listed as recipients`;
|
|
1004
|
+
return (
|
|
1005
|
+
`Error: cannot decrypt ${target}: this host's ${matchDesc}, but sops still refused. ` +
|
|
1006
|
+
'Common causes: the encrypted store predates this recipient stanza ' +
|
|
1007
|
+
'(someone with decrypt access must run `sops updatekeys` on it), ' +
|
|
1008
|
+
'or the key file permissions block sops from reading it. ' +
|
|
1009
|
+
`${recipientsDesc}. Caller key file: ${callerDesc}.`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (callerPubs.length === 0) {
|
|
1013
|
+
return (
|
|
1014
|
+
`Error: cannot decrypt ${target}: ${callerDesc}. ${recipientsDesc}. ` +
|
|
1015
|
+
"Fix: restore your age key from backup, or run 'secrets init' to start a fresh vault."
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return (
|
|
1019
|
+
`Error: cannot decrypt ${target}: this host's ${callerDesc} is not in the recipient list; ` +
|
|
1020
|
+
`${recipientsDesc}. ` +
|
|
1021
|
+
"Fix: add this host's public key to the scope's recipients, or run this verb from a host that already has decrypt access."
|
|
1022
|
+
);
|
|
840
1023
|
}
|
|
841
1024
|
|
|
842
1025
|
/**
|
|
@@ -21,7 +21,10 @@ Implemented: `generate`, `generate --dry-run`, `--help`.
|
|
|
21
21
|
Bundled templates:
|
|
22
22
|
|
|
23
23
|
- `dave-boot` — the original use case. Six-section operator-boot card aimed
|
|
24
|
-
at single-operator PA instances like Dave.
|
|
24
|
+
at single-operator PA instances like Dave. Sections: Operator basics,
|
|
25
|
+
Mission framing, Active relationships, Operational constraints, Tools at
|
|
26
|
+
your disposal, Live state. Post-generate lint enforces the 2026-09-03
|
|
27
|
+
contract (w-2026-07-15-012 / w-2026-09-04-dave-003).
|
|
25
28
|
|
|
26
29
|
Custom templates are loaded with `--template <path>`.
|
|
27
30
|
|
|
@@ -59,14 +62,26 @@ Optional inputs:
|
|
|
59
62
|
with `shell: false` — no shell interpolation, the inputs are safe even if
|
|
60
63
|
they contain shell metacharacters.
|
|
61
64
|
4. Validate the response against the required-headers contract (six
|
|
62
|
-
sections: Operator basics, Mission framing, Active
|
|
63
|
-
|
|
65
|
+
sections: Operator basics, Mission framing, Active relationships,
|
|
66
|
+
Operational constraints, Tools at your disposal, Live state). A response
|
|
64
67
|
missing any required header is treated as a generator failure (exit 2)
|
|
65
|
-
and no file is written.
|
|
68
|
+
and no file is written. The 2026-09-03 rewrite (w-2026-07-15-012)
|
|
69
|
+
retired the Active projects and Load-bearing rules sections: live boot
|
|
70
|
+
state is injected by the `boot-context` hook, and operational rules live
|
|
71
|
+
in `operator/knowledge/operator-interface/operational-constraints.md`.
|
|
66
72
|
5. Validate the size against the 2 KB soft budget. Over-budget responses
|
|
67
|
-
produce a stderr warning
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
produce a stderr warning; the slice is still written when the byte
|
|
74
|
+
count sits inside the lint hard ceiling (`SIZE_LINT_HARD_BYTES`, 3000
|
|
75
|
+
B). Above that ceiling the post-generate lint refuses the write.
|
|
76
|
+
6. Run the template-specific post-generate lint (dave-boot: names the
|
|
77
|
+
platform package `@stravica-ai/digital-operator-pa`, points at the
|
|
78
|
+
constraints doc, carries no `## Active projects` or `## In-flight work`
|
|
79
|
+
block, size within the hard ceiling). A lint failure refuses the write
|
|
80
|
+
with exit 2 so a defective regeneration cannot clobber the good slice
|
|
81
|
+
in place. Pass `--no-lint` to bypass. Custom templates loaded with
|
|
82
|
+
`--template <path>` have no template-specific checks and always
|
|
83
|
+
proceed to write. w-2026-09-04-dave-003.
|
|
84
|
+
7. Write the response to the configured `--output` path. Parent directories
|
|
70
85
|
are created if missing.
|
|
71
86
|
|
|
72
87
|
#### Flags
|
|
@@ -80,6 +95,8 @@ Optional inputs:
|
|
|
80
95
|
this file. Default-off. See **Audit log** below.
|
|
81
96
|
- `--dry-run` Build the prompt and report; skip the claude
|
|
82
97
|
call and the file write.
|
|
98
|
+
- `--no-lint` Bypass the post-generate template-specific
|
|
99
|
+
lint. See step 6 above.
|
|
83
100
|
- `-h`, `--help` Show usage.
|
|
84
101
|
|
|
85
102
|
### Exit codes
|
|
@@ -105,6 +122,9 @@ Status vocabulary (closed):
|
|
|
105
122
|
- `noop-dry-run` — `--dry-run` path; no claude call, no file write.
|
|
106
123
|
- `fail-claude` — claude exited non-zero (transport failure, exit 3).
|
|
107
124
|
- `fail-validation` — claude returned content missing required headers (exit 2).
|
|
125
|
+
- `fail-lint`: post-generate template lint refused the write (exit 2). The
|
|
126
|
+
regeneration was blocked from clobbering the good slice in place.
|
|
127
|
+
w-2026-09-04-dave-003.
|
|
108
128
|
- `fail-prereq` — input loading failed (missing profile / observations / template, exit 2).
|
|
109
129
|
- `fail-write` — writing the slice output file failed (permissions, ENOSPC, etc., exit 2).
|
|
110
130
|
- `fail-usage` — early arg-validation failed (missing required option, exit 1).
|
|
@@ -134,8 +154,9 @@ A single markdown file at the `--output` path. Plain markdown, no
|
|
|
134
154
|
front-matter, no JSON envelope. The PA's boot rule reads it the same way it
|
|
135
155
|
reads the full profile.
|
|
136
156
|
|
|
137
|
-
Soft target: under 2 KB / ~30 lines. Hard
|
|
138
|
-
headers must be present
|
|
157
|
+
Soft target: under 2 KB / ~30 lines. Hard requirements: the six section
|
|
158
|
+
headers must be present, and (for the bundled `dave-boot` template) the
|
|
159
|
+
post-generate lint contract in step 6.
|
|
139
160
|
|
|
140
161
|
## Design notes
|
|
141
162
|
|
|
@@ -33,6 +33,16 @@ Options for "generate":
|
|
|
33
33
|
slice runs on a schedule, point this at a
|
|
34
34
|
per-task runs.jsonl. Optional.
|
|
35
35
|
--dry-run Build the prompt and report; skip claude call + write.
|
|
36
|
+
--no-lint Bypass the post-generate template-specific lint.
|
|
37
|
+
The lint refuses to write a slice that fails the
|
|
38
|
+
template's contract checks (dave-boot: names the
|
|
39
|
+
platform package, points at the constraints doc,
|
|
40
|
+
no baked in-flight state, size within ceiling).
|
|
41
|
+
The scheduled nightly leaves this OFF so a
|
|
42
|
+
defective regeneration cannot clobber the good
|
|
43
|
+
slice; hand-invocations may pass it when the
|
|
44
|
+
checks are known to be wrong for the current
|
|
45
|
+
contract.
|
|
36
46
|
-h, --help Show this help.
|
|
37
47
|
|
|
38
48
|
Exit codes:
|
|
@@ -89,6 +99,7 @@ async function route(argv, { stderr, stdout, exit }) {
|
|
|
89
99
|
template: { type: 'string' },
|
|
90
100
|
'runs-log': { type: 'string' },
|
|
91
101
|
'dry-run': { type: 'boolean', default: false },
|
|
102
|
+
'no-lint': { type: 'boolean', default: false },
|
|
92
103
|
help: { type: 'boolean', short: 'h', default: false },
|
|
93
104
|
},
|
|
94
105
|
strict: true,
|
|
@@ -126,6 +137,7 @@ async function route(argv, { stderr, stdout, exit }) {
|
|
|
126
137
|
outputPath: values.output,
|
|
127
138
|
runsLog: values['runs-log'],
|
|
128
139
|
dryRun: values['dry-run'],
|
|
140
|
+
noLint: values['no-lint'],
|
|
129
141
|
runner: defaultRunner,
|
|
130
142
|
stderr,
|
|
131
143
|
});
|
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
You are operating as a compression assistant. Your job is to produce a compact "boot slice" of the operator's profile
|
|
1
|
+
You are operating as a compression assistant. Your job is to produce a compact "boot slice" of the operator's profile: a short markdown document that loads into the PA's context at every session start, in place of the full profile.
|
|
2
2
|
|
|
3
3
|
The full profile is the source of truth. This slice is a fresh snapshot, regenerated periodically. Stale content in the slice is acceptable for ~24 hours.
|
|
4
4
|
|
|
5
5
|
## What the slice is for
|
|
6
6
|
|
|
7
|
-
When the PA boots, it needs to know
|
|
7
|
+
When the PA boots, it needs to know, in roughly 1 to 2 KB of context, *who it is working for*. Not every preference, not every historical decision, not every technical opinion. Just enough that:
|
|
8
8
|
|
|
9
9
|
- It addresses the operator correctly (name, register, tone).
|
|
10
10
|
- It knows the operator's mission framing this year.
|
|
11
|
-
- It knows
|
|
11
|
+
- It knows the standing relationships that carry across sessions.
|
|
12
12
|
- It is aware of the tools and capabilities already built and available.
|
|
13
|
-
- It
|
|
14
|
-
- It can read the rest of the profile or knowledge base when it needs detail.
|
|
13
|
+
- It picks up operational constraints from the canonical constraints doc, not from a stale slice copy.
|
|
15
14
|
|
|
16
|
-
This is a navigation card, not the territory. **Prefer pointers to canonical sources over baked-in content** that will go stale between regenerations.
|
|
15
|
+
This is a navigation card, not the territory. **Prefer pointers to canonical sources over baked-in content** that will go stale between regenerations. Live boot state (active projects, in-flight work) is injected by the `boot-context` hook at session start, so the slice must NOT carry that state.
|
|
17
16
|
|
|
18
17
|
## Inputs you have
|
|
19
18
|
|
|
20
19
|
You are given:
|
|
21
20
|
|
|
22
|
-
1. **The full operator profile** (`profile.md`)
|
|
23
|
-
2. **Recent corrections / load-bearing rules** (optional, `learned-rules.md`)
|
|
24
|
-
3. **Recent observations** (optional)
|
|
21
|
+
1. **The full operator profile** (`profile.md`): the source of truth, ~18 KB.
|
|
22
|
+
2. **Recent corrections / load-bearing rules** (optional, `learned-rules.md`): ~50 KB of operational rules accumulated from corrections. Use only to inform vocabulary and register; do NOT bake rules into the slice.
|
|
23
|
+
3. **Recent observations** (optional): a concatenated dump of raw observation files filed in the last 2 to 4 weeks, useful for "what's been on his mind lately" but not authoritative.
|
|
25
24
|
|
|
26
|
-
Treat the profile as authoritative. Treat learned-rules as second-tier authoritative (they ARE rules). Treat observations as raw signal
|
|
25
|
+
Treat the profile as authoritative. Treat learned-rules as second-tier authoritative (they ARE rules, but they live in their own file: do not re-state them here). Treat observations as raw signal: synthesise, do not quote.
|
|
27
26
|
|
|
28
27
|
## Output structure
|
|
29
28
|
|
|
30
|
-
Produce a markdown document with exactly these
|
|
29
|
+
Produce a markdown document with exactly these six sections, in this order:
|
|
31
30
|
|
|
32
31
|
```
|
|
33
32
|
# Operator Boot Slice -- <Operator Name>
|
|
@@ -35,56 +34,59 @@ Produce a markdown document with exactly these seven sections, in this order:
|
|
|
35
34
|
_Generated <ISO timestamp>. Refreshed periodically. Source of truth: operator/profile/profile.md._
|
|
36
35
|
|
|
37
36
|
## Operator basics
|
|
38
|
-
<3
|
|
37
|
+
<3 to 5 bullets: name, what they go by, communication register, tone preferences, sensitivities>
|
|
39
38
|
|
|
40
39
|
## Mission framing
|
|
41
|
-
<1
|
|
42
|
-
|
|
43
|
-
## Active projects
|
|
44
|
-
<3-6 bullets, one line each: project name -- one-line status>
|
|
40
|
+
<1 to 2 lines: the north-star and what the operator is building toward this year. Name the PA platform's ship-vehicle plainly. The platform ships as `@stravica-ai/digital-operator-pa` on npm; if the profile confirms this, say so.>
|
|
45
41
|
|
|
46
42
|
## Active relationships
|
|
47
|
-
<2
|
|
43
|
+
<2 to 4 bullets, one line each: who -- relationship context. Standing relationships only, not one-off collaborations.>
|
|
44
|
+
|
|
45
|
+
## Operational constraints (session-load)
|
|
46
|
+
|
|
47
|
+
Read `operator/knowledge/operator-interface/operational-constraints.md` on every boot. That doc carries network, infrastructure, tooling, estate and voice constraints and standing corrections. It is the one place these live; anything you find restated in this slice or in `profile.md` is a defect, escalate it under the routing rule in `operator/knowledge/knowledge-memory/share-scope-routing.md`.
|
|
48
48
|
|
|
49
49
|
## Tools at your disposal
|
|
50
|
-
<2-3 lines: a one-line summary of the HQ-built operator-extension tools you have available (look for evidence of these in the inputs — names like wsd-ingest, dispatch, svc, monitoring, scheduled cron tasks, etc.), followed by a pointer line to the canonical list:>
|
|
51
|
-
For the canonical inventory of operator-built tools and scheduled tasks, read `operator/knowledge/systems/operator-tools.md`.
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
<2 to 3 lines: a one-line summary of the platform bins (via `pnpm exec <bin>`; the platform ships as `@stravica-ai/digital-operator-pa`) and the HQ operator-extension tools available, followed by a pointer line:>
|
|
52
|
+
For the current tool surface each session, discover via `ls operator/extensions/tools/` and `pnpm exec pa --help`; never rely on memory.
|
|
53
|
+
|
|
54
|
+
## Live state -- fetch on boot
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
For
|
|
58
|
-
|
|
56
|
+
For current projects and in-flight work items: `pnpm exec desk overview`.
|
|
57
|
+
For the last session's boundary notes: `agents/dave/next-session.md`.
|
|
58
|
+
For recent dispatches: tail `agents/dave/spawn-log.json`.
|
|
59
|
+
The `boot-context` hook injects a live `workState` block on session start; trust that first, hunt files only if the hook did not fire.
|
|
60
|
+
Do not bake state into this slice, it will be wrong by the time it is read.
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
The
|
|
63
|
+
The **Operational constraints** section and the **Live state** section are **fixed instructions**, not generated content. Reproduce them verbatim. The point is to send the PA to the canonical constraints doc for any operational rule, and to the live tools for any state question, rather than relying on a frozen snapshot.
|
|
62
64
|
|
|
63
65
|
## Budget
|
|
64
66
|
|
|
65
|
-
**Soft target: under 2 KB total output.** Aim for ~1.2
|
|
67
|
+
**Soft target: under 2 KB total output.** Aim for ~1.2 to 1.8 KB. If a section will not fit in the budget, prefer fewer items over shorter items: better to give 3 sharp lines than 6 mushy ones. Over-budget is warned; under-information is more costly.
|
|
66
68
|
|
|
67
69
|
## Style
|
|
68
70
|
|
|
69
71
|
- No emojis (the operator has a strict no-emoji rule for persisted artefacts).
|
|
70
|
-
- No em-dashes
|
|
72
|
+
- No em-dashes anywhere. Use a comma, a full stop, or a colon. The double-hyphen `--` in the file title and the "Live state -- fetch on boot" heading is intentional and must be reproduced verbatim.
|
|
71
73
|
- Use the operator's own vocabulary where you can pull it cleanly from the profile.
|
|
72
|
-
-
|
|
74
|
+
- Do not editorialise. State, do not sell.
|
|
73
75
|
- "Operator" not "user". "PA" or the PA's name, not "the assistant".
|
|
74
76
|
|
|
75
77
|
## What NOT to include
|
|
76
78
|
|
|
77
|
-
- Technical opinions (the full Technical Opinions section of profile.md).
|
|
79
|
+
- Technical opinions (the full Technical Opinions section of `profile.md`).
|
|
78
80
|
- Historical decision patterns.
|
|
79
|
-
-
|
|
81
|
+
- A "Load-bearing rules" or "Active projects" or "In-flight work" block: the constraints doc and the `boot-context` hook cover those; a baked copy here goes stale within hours.
|
|
82
|
+
- Communication-style examples that are not load-bearing.
|
|
80
83
|
- Anything the PA can look up at the moment it needs it (this is what the pointers are for).
|
|
81
|
-
- A baked snapshot of project state — the Live-state section's pointer covers that.
|
|
82
84
|
|
|
83
|
-
The test for "should this be in the slice": *if this is wrong or missing, will the PA make a costly mistake in its first 5 messages?* If yes, include it. If no, leave it out.
|
|
85
|
+
The test for "should this be in the slice": *if this is wrong or missing, will the PA make a costly mistake in its first 5 messages, AND is it something the constraints doc plus the boot-context hook cannot cover?* If yes to both, include it. If no, leave it out.
|
|
84
86
|
|
|
85
87
|
## Now produce the slice
|
|
86
88
|
|
|
87
|
-
The inputs follow below. Read them, then produce the slice. Output ONLY the slice markdown
|
|
89
|
+
The inputs follow below. Read them, then produce the slice. Output ONLY the slice markdown: no preamble, no postscript, no explanation. Your entire response is what gets written to disk.
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|