@stravica-ai/digital-operator-pa 0.1.13 → 0.1.14
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/README.md +18 -5
- package/dist/engineering-pa-cli/src/agent/audit.mjs +77 -0
- package/dist/engineering-pa-cli/src/agent/cli.mjs +266 -0
- package/dist/engineering-pa-cli/src/agent/probes.mjs +236 -0
- package/dist/engineering-pa-cli/src/autonomy/cli.mjs +1 -1
- package/dist/engineering-pa-cli/src/autonomy/envelope.mjs +19 -300
- package/dist/engineering-pa-cli/src/pa/cli.mjs +8 -0
- package/dist/engineering-pa-core/index.mjs +13 -0
- package/dist/engineering-pa-core/knowledge/scaffold/README.md +66 -0
- package/dist/engineering-pa-core/knowledge/scaffold/agent-baseline.json +132 -0
- package/dist/engineering-pa-core/knowledge/scaffold/agent-baseline.schema.json +146 -0
- package/dist/engineering-pa-core/src/agent-baseline/manifest.mjs +139 -0
- package/dist/engineering-pa-core/src/autonomy/envelope.mjs +301 -0
- package/dist/engineering-pa-scaffold/package.json +3 -0
- package/dist/engineering-pa-scaffold/src/generator.mjs +23 -0
- package/package.json +2 -2
|
@@ -1,301 +1,20 @@
|
|
|
1
|
-
// envelope.mjs —
|
|
2
|
-
// rendering. Zero IO. All filesystem side effects live in cli.mjs.
|
|
1
|
+
// envelope.mjs — re-export shim.
|
|
3
2
|
//
|
|
4
|
-
// The envelope
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* after the closing `---`. Throws on malformed input; the error carries an
|
|
23
|
-
* `exitCode` for the CLI to propagate.
|
|
24
|
-
*/
|
|
25
|
-
export function parseEnvelope(source) {
|
|
26
|
-
if (typeof source !== 'string' || source.length === 0) {
|
|
27
|
-
throwUsage('envelope file is empty');
|
|
28
|
-
}
|
|
29
|
-
const lines = source.split(/\r?\n/);
|
|
30
|
-
if (lines[0].trim() !== '---') {
|
|
31
|
-
throwParse('envelope must start with YAML frontmatter delimited by ---');
|
|
32
|
-
}
|
|
33
|
-
let closeIdx = -1;
|
|
34
|
-
for (let i = 1; i < lines.length; i++) {
|
|
35
|
-
if (lines[i].trim() === '---') {
|
|
36
|
-
closeIdx = i;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (closeIdx < 0) {
|
|
41
|
-
throwParse('unterminated YAML frontmatter (missing closing ---)');
|
|
42
|
-
}
|
|
43
|
-
const fmLines = lines.slice(1, closeIdx);
|
|
44
|
-
const bodyLines = lines.slice(closeIdx + 1);
|
|
45
|
-
const frontmatter = parseFrontmatter(fmLines);
|
|
46
|
-
return { frontmatter, body: bodyLines.join('\n') };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parse a limited-shape YAML block. Supports:
|
|
51
|
-
* key: value (string)
|
|
52
|
-
* key: (empty list start)
|
|
53
|
-
* - key: value (list item — object)
|
|
54
|
-
* key: value
|
|
55
|
-
* Numbers, booleans, and nested maps beyond one level are NOT supported —
|
|
56
|
-
* the schema doesn't need them. Values are always treated as strings.
|
|
57
|
-
*/
|
|
58
|
-
function parseFrontmatter(lines) {
|
|
59
|
-
const out = {};
|
|
60
|
-
let i = 0;
|
|
61
|
-
while (i < lines.length) {
|
|
62
|
-
const raw = lines[i];
|
|
63
|
-
if (raw.trim() === '' || raw.trim().startsWith('#')) { i++; continue; }
|
|
64
|
-
const match = raw.match(/^([a-zA-Z][a-zA-Z0-9]*):(?:\s*(.*))?$/);
|
|
65
|
-
if (!match) {
|
|
66
|
-
throwParse(`could not parse frontmatter line ${i + 1}: ${raw}`);
|
|
67
|
-
}
|
|
68
|
-
const key = match[1];
|
|
69
|
-
const value = (match[2] ?? '').trim();
|
|
70
|
-
if (value === '') {
|
|
71
|
-
// Expect either a list (starts with "- " at next non-empty indented
|
|
72
|
-
// line) or an empty value.
|
|
73
|
-
const listItems = [];
|
|
74
|
-
let j = i + 1;
|
|
75
|
-
while (j < lines.length) {
|
|
76
|
-
const peek = lines[j];
|
|
77
|
-
if (peek.trim() === '' || peek.trim().startsWith('#')) { j++; continue; }
|
|
78
|
-
if (!/^\s+-\s/.test(peek)) break;
|
|
79
|
-
const item = readListItem(lines, j);
|
|
80
|
-
listItems.push(item.value);
|
|
81
|
-
j = item.next;
|
|
82
|
-
}
|
|
83
|
-
out[key] = listItems;
|
|
84
|
-
i = j;
|
|
85
|
-
} else {
|
|
86
|
-
out[key] = stripQuotes(value);
|
|
87
|
-
i++;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return out;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function readListItem(lines, start) {
|
|
94
|
-
const first = lines[start];
|
|
95
|
-
const indentMatch = first.match(/^(\s+)-\s/);
|
|
96
|
-
if (!indentMatch) {
|
|
97
|
-
throwParse(`expected list item at line ${start + 1}: ${first}`);
|
|
98
|
-
}
|
|
99
|
-
const itemIndent = indentMatch[1] + ' '; // continuation is deeper than "- "
|
|
100
|
-
const item = {};
|
|
101
|
-
// The "- key: value" first line is the first key on the item.
|
|
102
|
-
const firstBody = first.slice(indentMatch[0].length);
|
|
103
|
-
const firstMatch = firstBody.match(/^([a-zA-Z][a-zA-Z0-9]*):\s*(.*)$/);
|
|
104
|
-
if (!firstMatch) {
|
|
105
|
-
throwParse(`could not parse list item at line ${start + 1}: ${first}`);
|
|
106
|
-
}
|
|
107
|
-
item[firstMatch[1]] = stripQuotes(firstMatch[2].trim());
|
|
108
|
-
let j = start + 1;
|
|
109
|
-
while (j < lines.length) {
|
|
110
|
-
const peek = lines[j];
|
|
111
|
-
if (peek.trim() === '' || peek.trim().startsWith('#')) { j++; continue; }
|
|
112
|
-
// Terminate when indent doesn't extend the item (either new list item
|
|
113
|
-
// at the same "- " indent, or dedent back to a top-level key).
|
|
114
|
-
if (peek.startsWith(itemIndent)) {
|
|
115
|
-
const bodyLine = peek.slice(itemIndent.length);
|
|
116
|
-
const m = bodyLine.match(/^([a-zA-Z][a-zA-Z0-9]*):\s*(.*)$/);
|
|
117
|
-
if (!m) {
|
|
118
|
-
throwParse(`could not parse list item continuation at line ${j + 1}: ${peek}`);
|
|
119
|
-
}
|
|
120
|
-
item[m[1]] = stripQuotes(m[2].trim());
|
|
121
|
-
j++;
|
|
122
|
-
} else {
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return { value: item, next: j };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function stripQuotes(value) {
|
|
130
|
-
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
131
|
-
return value.slice(1, -1);
|
|
132
|
-
}
|
|
133
|
-
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
134
|
-
return value.slice(1, -1);
|
|
135
|
-
}
|
|
136
|
-
return value;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Render an envelope object (frontmatter shape) back to markdown. Body is
|
|
141
|
-
* regenerated: a current-state table + a history log preserved from prior
|
|
142
|
-
* body sections. This keeps the file readable + deterministic.
|
|
143
|
-
*/
|
|
144
|
-
export function renderEnvelope(frontmatter, history = []) {
|
|
145
|
-
const fm = renderFrontmatter(frontmatter);
|
|
146
|
-
const body = renderBody(frontmatter, history);
|
|
147
|
-
return `---\n${fm}---\n\n${body}\n`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderFrontmatter(fm) {
|
|
151
|
-
const lines = [];
|
|
152
|
-
lines.push(`agent: ${quoteIfNeeded(fm.agent)}`);
|
|
153
|
-
lines.push(`owner: ${quoteIfNeeded(fm.owner)}`);
|
|
154
|
-
lines.push(`updated: ${quoteIfNeeded(fm.updated)}`);
|
|
155
|
-
lines.push('taskTypes:');
|
|
156
|
-
const items = fm.taskTypes ?? [];
|
|
157
|
-
for (const item of items) {
|
|
158
|
-
lines.push(` - id: ${quoteIfNeeded(item.id)}`);
|
|
159
|
-
lines.push(` label: ${quoteIfNeeded(item.label)}`);
|
|
160
|
-
lines.push(` state: ${quoteIfNeeded(item.state)}`);
|
|
161
|
-
if (item.stateSince) lines.push(` stateSince: ${quoteIfNeeded(item.stateSince)}`);
|
|
162
|
-
if (item.operatorNote) lines.push(` operatorNote: ${quoteIfNeeded(item.operatorNote)}`);
|
|
163
|
-
}
|
|
164
|
-
return lines.join('\n') + '\n';
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function quoteIfNeeded(value) {
|
|
168
|
-
if (value == null) return '""';
|
|
169
|
-
const str = String(value);
|
|
170
|
-
// Quote values that could be misparsed as YAML control sequences.
|
|
171
|
-
if (/^[\s"'#\-\[\{,&*!|>%@`]/.test(str)) return JSON.stringify(str);
|
|
172
|
-
if (/[:#]\s/.test(str)) return JSON.stringify(str);
|
|
173
|
-
if (str.trim() !== str) return JSON.stringify(str);
|
|
174
|
-
return str;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function renderBody(fm, history) {
|
|
178
|
-
const lines = [];
|
|
179
|
-
lines.push(`# Autonomy envelope — ${fm.agent}`);
|
|
180
|
-
lines.push('');
|
|
181
|
-
lines.push('## Current state');
|
|
182
|
-
lines.push('');
|
|
183
|
-
if (!fm.taskTypes || fm.taskTypes.length === 0) {
|
|
184
|
-
lines.push('_No task types configured. Use `pa autonomy set <agent> <task-type> <state>` to add one._');
|
|
185
|
-
} else {
|
|
186
|
-
lines.push('| Task type | State | Since | Note |');
|
|
187
|
-
lines.push('|---|---|---|---|');
|
|
188
|
-
for (const item of fm.taskTypes) {
|
|
189
|
-
const since = (item.stateSince ?? '').slice(0, 10);
|
|
190
|
-
const note = (item.operatorNote ?? '—').replace(/\|/g, '\\|');
|
|
191
|
-
lines.push(`| ${escapeCell(item.label)} | ${STATE_LABELS[item.state] ?? item.state} | ${since || '—'} | ${note} |`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
lines.push('');
|
|
195
|
-
lines.push('## History');
|
|
196
|
-
lines.push('');
|
|
197
|
-
const entries = [...history].sort((a, b) => (b.at ?? '').localeCompare(a.at ?? ''));
|
|
198
|
-
if (entries.length === 0) {
|
|
199
|
-
lines.push('_No history yet._');
|
|
200
|
-
} else {
|
|
201
|
-
for (const entry of entries) {
|
|
202
|
-
lines.push(`### ${entry.at} — ${entry.taskTypeId} → ${entry.state}`);
|
|
203
|
-
if (entry.note) {
|
|
204
|
-
lines.push('');
|
|
205
|
-
lines.push(`Operator note: ${entry.note}`);
|
|
206
|
-
}
|
|
207
|
-
lines.push('');
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return lines.join('\n');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function escapeCell(str) {
|
|
214
|
-
return String(str).replace(/\|/g, '\\|');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Extract history entries from a rendered body. Returns an array of
|
|
219
|
-
* { at, taskTypeId, state, note }
|
|
220
|
-
* entries. Best-effort — malformed sections are skipped, not raised.
|
|
221
|
-
*/
|
|
222
|
-
export function extractHistory(body) {
|
|
223
|
-
const entries = [];
|
|
224
|
-
if (typeof body !== 'string') return entries;
|
|
225
|
-
const lines = body.split(/\r?\n/);
|
|
226
|
-
let i = 0;
|
|
227
|
-
while (i < lines.length) {
|
|
228
|
-
const line = lines[i];
|
|
229
|
-
const match = line.match(/^###\s+(\S+)\s+—\s+([a-z0-9][a-z0-9-]*)\s+→\s+([A-Za-z]+)\s*$/);
|
|
230
|
-
if (!match) { i++; continue; }
|
|
231
|
-
const at = match[1];
|
|
232
|
-
const taskTypeId = match[2];
|
|
233
|
-
const state = match[3];
|
|
234
|
-
let note = null;
|
|
235
|
-
let j = i + 1;
|
|
236
|
-
while (j < lines.length && lines[j].trim() === '') j++;
|
|
237
|
-
if (j < lines.length && lines[j].startsWith('Operator note:')) {
|
|
238
|
-
note = lines[j].slice('Operator note:'.length).trim();
|
|
239
|
-
}
|
|
240
|
-
entries.push({ at, taskTypeId, state, note });
|
|
241
|
-
i = j + 1;
|
|
242
|
-
}
|
|
243
|
-
return entries;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Validate a frontmatter object against the schema. Throws on invalid;
|
|
248
|
-
* returns nothing on valid.
|
|
249
|
-
*/
|
|
250
|
-
export function validateFrontmatter(fm) {
|
|
251
|
-
if (!fm || typeof fm !== 'object') throwParse('frontmatter must be an object');
|
|
252
|
-
requireString(fm, 'agent');
|
|
253
|
-
requireString(fm, 'owner');
|
|
254
|
-
requireString(fm, 'updated');
|
|
255
|
-
if (!Array.isArray(fm.taskTypes)) throwParse('taskTypes must be an array');
|
|
256
|
-
for (const [i, item] of fm.taskTypes.entries()) {
|
|
257
|
-
if (!item || typeof item !== 'object') throwParse(`taskTypes[${i}] must be an object`);
|
|
258
|
-
requireString(item, `taskTypes[${i}].id`, item.id);
|
|
259
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(item.id)) throwParse(`taskTypes[${i}].id '${item.id}' must match ^[a-z0-9][a-z0-9-]*$`);
|
|
260
|
-
requireString(item, `taskTypes[${i}].label`, item.label);
|
|
261
|
-
requireString(item, `taskTypes[${i}].state`, item.state);
|
|
262
|
-
if (!VALID_STATES.includes(item.state)) {
|
|
263
|
-
throwParse(`taskTypes[${i}].state '${item.state}' must be one of ${VALID_STATES.join(', ')}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function requireString(obj, field, value) {
|
|
269
|
-
const v = value ?? obj?.[field];
|
|
270
|
-
if (typeof v !== 'string' || v.length === 0) {
|
|
271
|
-
throwParse(`${field} must be a non-empty string`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function throwParse(msg) {
|
|
276
|
-
const err = new Error(msg);
|
|
277
|
-
err.exitCode = 2;
|
|
278
|
-
throw err;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function throwUsage(msg) {
|
|
282
|
-
const err = new Error(msg);
|
|
283
|
-
err.exitCode = 1;
|
|
284
|
-
throw err;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export function nowIso() {
|
|
288
|
-
return new Date().toISOString();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function emptyEnvelope(agent, owner) {
|
|
292
|
-
return {
|
|
293
|
-
frontmatter: {
|
|
294
|
-
agent,
|
|
295
|
-
owner,
|
|
296
|
-
updated: nowIso(),
|
|
297
|
-
taskTypes: [],
|
|
298
|
-
},
|
|
299
|
-
history: [],
|
|
300
|
-
};
|
|
301
|
-
}
|
|
3
|
+
// The envelope parse/render/seed logic lives in
|
|
4
|
+
// `@stravica-ai/engineering-pa-core/src/autonomy/envelope.mjs` — pure, no
|
|
5
|
+
// CLI coupling, so scaffold can seed a round-trippable stub without an
|
|
6
|
+
// inverted package dependency (scaffold → core, not scaffold → cli).
|
|
7
|
+
//
|
|
8
|
+
// This shim exists so anything that historically imported from
|
|
9
|
+
// `packages/cli/src/autonomy/envelope.mjs` (tests, internal callers)
|
|
10
|
+
// keeps working. New callers should import from core directly.
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
VALID_STATES,
|
|
14
|
+
parseEnvelope,
|
|
15
|
+
renderEnvelope,
|
|
16
|
+
extractHistory,
|
|
17
|
+
validateFrontmatter,
|
|
18
|
+
nowIso,
|
|
19
|
+
emptyEnvelope,
|
|
20
|
+
} from '../../../engineering-pa-core/src/autonomy/envelope.mjs';
|
|
@@ -27,6 +27,7 @@ Usage:
|
|
|
27
27
|
pa research <verb> Deep-research session store (run | list | show | gates | prune | mark | paths)
|
|
28
28
|
pa autonomy <verb> Per-agent per-task-type approval state (init | get | set | list)
|
|
29
29
|
pa environment [verb] Session-boot environment cache (default | refresh | extended)
|
|
30
|
+
pa agent <verb> Agent baseline audit (audit | audit refresh)
|
|
30
31
|
pa staging-alias <verb> Canonical staging.<site> alias for CF Workers sites (set | unset | status | list)
|
|
31
32
|
pa lint <verb> Lint operator text against voice rules (linkedin)
|
|
32
33
|
pa migrate-inspect [options] Host inspection for migration planning
|
|
@@ -76,6 +77,8 @@ export async function main(argv) {
|
|
|
76
77
|
return await doAutonomy(rest);
|
|
77
78
|
case 'environment':
|
|
78
79
|
return await doEnvironment(rest);
|
|
80
|
+
case 'agent':
|
|
81
|
+
return await doAgent(rest);
|
|
79
82
|
case 'lint':
|
|
80
83
|
return await doLint(rest);
|
|
81
84
|
case 'staging-alias':
|
|
@@ -276,6 +279,11 @@ async function doEnvironment(rest) {
|
|
|
276
279
|
return await run(rest);
|
|
277
280
|
}
|
|
278
281
|
|
|
282
|
+
async function doAgent(rest) {
|
|
283
|
+
const { run } = await import('../agent/cli.mjs');
|
|
284
|
+
return await run(rest);
|
|
285
|
+
}
|
|
286
|
+
|
|
279
287
|
async function doStagingAlias(rest) {
|
|
280
288
|
const { main } = await import('../staging-alias/cli.mjs');
|
|
281
289
|
return await main(rest);
|
|
@@ -19,3 +19,16 @@ export { acquireLock, DEFAULT_STALE_AFTER_MS } from './src/lock/lock.mjs';
|
|
|
19
19
|
// Distill prereqs / prompt — also useful as a library entry point.
|
|
20
20
|
export { resolvePrereqs, defaultRepoRoot as distillDefaultRepoRoot, BUNDLED_LIBRARIAN_DEF } from './src/distill/prereqs.mjs';
|
|
21
21
|
export { buildDistillPrompt, DISTILL_PROMPT, claudeArgs } from './src/distill/prompt.mjs';
|
|
22
|
+
|
|
23
|
+
// Agent baseline manifest — machine-readable list of capabilities every
|
|
24
|
+
// operator PA must carry. Consumed by `pa agent audit` (drift detection)
|
|
25
|
+
// and by `pa-init` (creation-time coverage check). Docs:
|
|
26
|
+
// packages/core/knowledge/scaffold/README.md.
|
|
27
|
+
export {
|
|
28
|
+
bundledManifestPath,
|
|
29
|
+
loadBundledManifest,
|
|
30
|
+
loadBundledManifestSync,
|
|
31
|
+
parseManifest,
|
|
32
|
+
expandTemplate,
|
|
33
|
+
capabilitySeverity,
|
|
34
|
+
} from './src/agent-baseline/manifest.mjs';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# scaffold — baseline manifest + scaffold seeds
|
|
2
|
+
|
|
3
|
+
Machine-readable baselines that shape every fresh PA and every audit against a running one.
|
|
4
|
+
|
|
5
|
+
## `agent-baseline.json`
|
|
6
|
+
|
|
7
|
+
The list of capabilities every operator PA is expected to carry. Each entry is a mechanical presence-probe: `pa agent audit` (packages/cli/src/agent/) diffs an agent estate against this file and reports gaps.
|
|
8
|
+
|
|
9
|
+
Two failure classes it covers, in priority order:
|
|
10
|
+
|
|
11
|
+
1. **Drift** — the platform grows a capability, existing agents never adopt. This is the motivating case. Boot-slice shipped in core 0.1.7, one estate adopted, another never noticed for months. The audit turns adoption into a generated to-do instead of a memory test.
|
|
12
|
+
2. **Creation-time gaps** — a new agent scaffolded by `pa-init` boots missing standard kit. `packages/scaffold/test/manifest-coverage.test.mjs` runs the manifest against the pa-init plan and fails the build if any agents-local capability with a static-path probe is uncovered.
|
|
13
|
+
|
|
14
|
+
### Version-coupling convention
|
|
15
|
+
|
|
16
|
+
Every capability entry carries a `sinceVersion` (the `@stravica-ai/engineering-pa-core` release that introduced it) and the manifest as a whole carries a `manifestVersion` integer.
|
|
17
|
+
|
|
18
|
+
**When a new capability lands:**
|
|
19
|
+
|
|
20
|
+
1. Add the entry to `agent-baseline.json`.
|
|
21
|
+
2. Bump `manifestVersion` by 1.
|
|
22
|
+
3. Set the entry's `sinceVersion` to the core version the change ships in.
|
|
23
|
+
4. Note the manifest bump in `CHANGELOG.md` under the release (line pattern: `agent-baseline: added <capability-id> at manifestVersion N`).
|
|
24
|
+
|
|
25
|
+
**When a capability is removed** (rare — retired platform feature): bump `manifestVersion` and note the removal in the changelog. Never rename an `id` — retire and add.
|
|
26
|
+
|
|
27
|
+
### Probe types (v1)
|
|
28
|
+
|
|
29
|
+
Each probe carries a `type` and its own required fields (see `agent-baseline.schema.json`):
|
|
30
|
+
|
|
31
|
+
| Type | Purpose |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `file-exists` | File at `path` exists. `{paName}` token substituted. |
|
|
34
|
+
| `directory-exists` | Directory at `path` exists (must be a dir, not a file). |
|
|
35
|
+
| `knowledge-doc-exists` | Semantic alias for `file-exists` on `operator/knowledge/**/*.md`. Kept as its own type so future extensions (graph presence, non-orphan) can enrich the check. |
|
|
36
|
+
| `staging-recent-write` | Directory exists AND has a non-hidden entry younger than `maxAgeDays`. Fails only if the newest entry is older than the window. Use with `severity: warn` — a quiet week is not a broken agent. |
|
|
37
|
+
| `schedule-defined` | A JSON file under `path` (typically `schedules/`) whose `command` includes ALL of `commandIncludes` and (if given) AT LEAST ONE of `commandIncludesAny`, and is not `enabled: false`. Host-pinned schedules produce `unverified-here` when the audited host doesn't match. Use with `hostDependent: true`. |
|
|
38
|
+
| `json-field-includes` | Open a JSON file at `path`, check that top-level `field` is an array containing `expectedValue` (token-substituted). Optional `requiredTool` gates the probe: absent tool = `unverified-here`. |
|
|
39
|
+
|
|
40
|
+
### Outcome ladder
|
|
41
|
+
|
|
42
|
+
| Rendered status | When |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `pass` | Probe matched. |
|
|
45
|
+
| `warn` | Probe failed AND capability has `severity: warn` — informational, never gates `--strict`. |
|
|
46
|
+
| `fail` | Probe failed AND capability has `severity: fail` (default). Gates `--strict`. |
|
|
47
|
+
| `unverified-here` | Mechanically indeterminate on this host — host-pinned capability declared for another host, required tool absent from the estate, probe threw. Never a false pass, never a false fail. |
|
|
48
|
+
|
|
49
|
+
### Named defaults (ratified with the operator on approval)
|
|
50
|
+
|
|
51
|
+
- `pa agent audit` exit code default = 0 (informational tool). `--strict` opts into CI-gating exit 2 on any `fail`.
|
|
52
|
+
- `staging-recent-write` window = 7 days, severity `warn` (quiet-week tolerance).
|
|
53
|
+
- Host-dependent probes report `unverified-here`, never `fail`. Precision over noise.
|
|
54
|
+
- Manifest ships in `packages/core/knowledge/scaffold/` (single-bundled via `packages/engineering-pa/build.mjs`), not a separate schema package.
|
|
55
|
+
- Cache path `.session/agent-audit.json`, mirroring `pa environment`'s `.session/environment.json`.
|
|
56
|
+
- `sinceVersion` is the `@stravica-ai/engineering-pa-core` version, not the platform tarball version (manifest lives in core).
|
|
57
|
+
|
|
58
|
+
### Consumers
|
|
59
|
+
|
|
60
|
+
- **`pa agent audit`** — session-boot drift detection. Reads the cache; auto-refreshes on stale.
|
|
61
|
+
- **`pa-init`** (via `manifest-coverage.test.mjs`) — creation-time coverage guard. New capabilities that CAN be scaffolded MUST be scaffolded, or the test fails and forces the scaffold plan update at the same time as the manifest bump.
|
|
62
|
+
- **Future: `boot-context`** — the session-boot hook can surface a one-liner (`3/11 capabilities need adoption`) into the injected workState block.
|
|
63
|
+
|
|
64
|
+
## `domains.json`
|
|
65
|
+
|
|
66
|
+
The base set of knowledge domains seeded into every fresh PA — unchanged by this pass.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./agent-baseline.schema.json",
|
|
3
|
+
"schemaVersion": "1.0",
|
|
4
|
+
"manifestVersion": 1,
|
|
5
|
+
"description": "Baseline capabilities every operator PA agent is expected to carry. Each entry is a mechanical presence-probe: `pa agent audit` diffs an agent estate against this manifest and reports gaps. New capabilities land here at the same time as the platform release that ships them, so drift becomes a generated to-do instead of a memory test. Version-coupling convention: bump `manifestVersion` whenever a new capability entry lands; set the capability's `sinceVersion` to the @stravica-ai/engineering-pa-core release that introduced it. Host-dependent probes MUST carry `hostDependent:true` so a mismatched host is reported as `unverified-here`, not a false fail.",
|
|
6
|
+
"capabilities": [
|
|
7
|
+
{
|
|
8
|
+
"id": "spawn-log",
|
|
9
|
+
"label": "Spawn-log JSON present",
|
|
10
|
+
"sinceVersion": "0.1.0",
|
|
11
|
+
"rationale": "Every dispatch must leave a trail; the spawn-log is the machine-readable record the librarian sweeps.",
|
|
12
|
+
"probe": {
|
|
13
|
+
"type": "file-exists",
|
|
14
|
+
"path": "agents/{paName}/spawn-log.json"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "journal",
|
|
19
|
+
"label": "Self-improvement journal present",
|
|
20
|
+
"sinceVersion": "0.1.0",
|
|
21
|
+
"rationale": "Every PA keeps a journal — the substrate for the librarian's distillation pass.",
|
|
22
|
+
"probe": {
|
|
23
|
+
"type": "file-exists",
|
|
24
|
+
"path": "agents/{paName}/journal.md"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "next-session",
|
|
29
|
+
"label": "Next-session handover file present",
|
|
30
|
+
"sinceVersion": "0.1.0",
|
|
31
|
+
"rationale": "Session-boundary continuity — the top of the file is operator-facing.",
|
|
32
|
+
"probe": {
|
|
33
|
+
"type": "file-exists",
|
|
34
|
+
"path": "agents/{paName}/next-session.md"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "autonomy-envelope",
|
|
39
|
+
"label": "Autonomy-envelope file present",
|
|
40
|
+
"sinceVersion": "0.1.0",
|
|
41
|
+
"rationale": "Per-task-type approval state — the durable record the PA checks when the reflex is unclear.",
|
|
42
|
+
"probe": {
|
|
43
|
+
"type": "file-exists",
|
|
44
|
+
"path": "agents/{paName}/autonomy-envelope.md"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "staging-dir",
|
|
49
|
+
"label": "Staging directory present",
|
|
50
|
+
"sinceVersion": "0.1.0",
|
|
51
|
+
"rationale": "Real-time observation staging — the librarian's inbox. Absent staging dir means the PA has no place to write in-session observations.",
|
|
52
|
+
"probe": {
|
|
53
|
+
"type": "directory-exists",
|
|
54
|
+
"path": "agents/{paName}/staging"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "staging-recent-write",
|
|
59
|
+
"label": "Staging directory has a recent write",
|
|
60
|
+
"sinceVersion": "0.1.0",
|
|
61
|
+
"rationale": "Warns (never fails) if the staging dir has been quiet for longer than the tolerated window — a signal that real-time staging discipline has drifted, but tolerant of legitimate quiet weeks.",
|
|
62
|
+
"severity": "warn",
|
|
63
|
+
"probe": {
|
|
64
|
+
"type": "staging-recent-write",
|
|
65
|
+
"path": "agents/{paName}/staging",
|
|
66
|
+
"maxAgeDays": 7
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "session-end-sop-overlay",
|
|
71
|
+
"label": "Session-boot-and-end SOP overlay present",
|
|
72
|
+
"sinceVersion": "0.1.0",
|
|
73
|
+
"rationale": "The estate-side SOP that layers over the core session-end routine — its absence means the PA is running on the core baseline only, with no estate customisation applied.",
|
|
74
|
+
"probe": {
|
|
75
|
+
"type": "knowledge-doc-exists",
|
|
76
|
+
"path": "operator/knowledge/operator-interface/session-boot-and-end.md"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "boot-slice-job",
|
|
81
|
+
"label": "Boot-slice schedule + output file",
|
|
82
|
+
"sinceVersion": "0.1.7",
|
|
83
|
+
"rationale": "The lightweight boot slice is what the PA loads on every session boot instead of the full profile. Its regeneration is scheduled on a profile-bearing host; the output file must exist somewhere in the estate.",
|
|
84
|
+
"hostDependent": true,
|
|
85
|
+
"probe": {
|
|
86
|
+
"type": "schedule-defined",
|
|
87
|
+
"path": "schedules",
|
|
88
|
+
"commandIncludes": ["slice generate"],
|
|
89
|
+
"outputPathHints": ["operator/profile/slices/"]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "library-sync-up",
|
|
94
|
+
"label": "Library-sync up-path scheduled",
|
|
95
|
+
"sinceVersion": "0.1.5",
|
|
96
|
+
"rationale": "Fresh estate content must reach canonical on every device that authors it; a missing up-path is the mechanism that produced the 2026-07-14 stale-clobber family.",
|
|
97
|
+
"hostDependent": true,
|
|
98
|
+
"probe": {
|
|
99
|
+
"type": "schedule-defined",
|
|
100
|
+
"path": "schedules",
|
|
101
|
+
"commandIncludes": ["library-sync"],
|
|
102
|
+
"commandIncludesAny": ["--up", "library-sync-cycle"]
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "library-sync-down",
|
|
107
|
+
"label": "Library-sync down-path scheduled",
|
|
108
|
+
"sinceVersion": "0.1.5",
|
|
109
|
+
"rationale": "Every device must materialise the freshest canonical content. On devices that use the cycle orchestrator the down leg is embedded; standalone down-only schedules also satisfy this probe.",
|
|
110
|
+
"hostDependent": true,
|
|
111
|
+
"probe": {
|
|
112
|
+
"type": "schedule-defined",
|
|
113
|
+
"path": "schedules",
|
|
114
|
+
"commandIncludes": ["library-sync"],
|
|
115
|
+
"commandIncludesAny": ["--down", "library-sync-cycle", "library-sync-refresh"]
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"id": "relay-agent-registration",
|
|
120
|
+
"label": "Agent id registered in claude-relay config",
|
|
121
|
+
"sinceVersion": "0.1.0",
|
|
122
|
+
"rationale": "Cross-agent transport requires the agent id to appear in `operator/extensions/tools/claude-relay/config.json:agents`; absence means no cross-machine relay traffic can address this PA.",
|
|
123
|
+
"probe": {
|
|
124
|
+
"type": "json-field-includes",
|
|
125
|
+
"path": "operator/extensions/tools/claude-relay/config.json",
|
|
126
|
+
"field": "agents",
|
|
127
|
+
"expectedValue": "{paName}",
|
|
128
|
+
"requiredTool": "claude-relay"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|