@stravica-ai/digital-operator-pa 0.1.66 → 0.1.68
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/docs/coordinator-hook.md +14 -1
- package/dist/engineering-pa-cli/src/agent/cli.mjs +18 -8
- package/dist/engineering-pa-cli/src/agent/probes.mjs +94 -0
- package/dist/engineering-pa-cli/src/pa/board-check.mjs +633 -0
- package/dist/engineering-pa-cli/src/pa/cli.mjs +52 -7
- package/dist/engineering-pa-cli/src/pa/dispatch-cli.mjs +655 -0
- package/dist/engineering-pa-cli/src/pa/repo-root.mjs +29 -10
- package/dist/engineering-pa-cli/src/pa/session.mjs +13 -16
- package/dist/engineering-pa-cli/src/pa/status-board-cli.mjs +425 -0
- package/dist/engineering-pa-cli/src/pa/status-board.mjs +263 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/delegation-modes.md +2 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/README.md +2 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/session-boot-and-end.md +8 -3
- package/dist/engineering-pa-core/knowledge/scaffold/agent-baseline.json +26 -1
- package/dist/engineering-pa-core/knowledge/scaffold/agent-baseline.schema.json +40 -1
- package/dist/engineering-pa-core/knowledge/work-management/README.md +2 -1
- package/dist/engineering-pa-core/knowledge/work-management/spawn-log-lock.md +167 -0
- package/dist/engineering-pa-core/knowledge/work-management/spawn-log.md +34 -6
- package/dist/engineering-pa-core/knowledge/work-management/status-board.md +80 -0
- package/dist/engineering-pa-core/src/agent-baseline/manifest.mjs +2 -0
- package/dist/engineering-pa-core/src/dispatch/label.mjs +23 -0
- package/dist/engineering-pa-core/src/spawn-log/read.mjs +355 -0
- package/dist/engineering-pa-core/src/spawn-log/write.mjs +763 -0
- package/dist/engineering-pa-scaffold/src/generator.mjs +178 -9
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +4 -1
- package/dist/engineering-pa-workspace/README.md +9 -0
- package/dist/engineering-pa-workspace/app/delegations.html +21 -9
- package/dist/engineering-pa-workspace/app/nav.json +6 -0
- package/dist/engineering-pa-workspace/app/notes.html +3 -1
- package/dist/engineering-pa-workspace/app/status-board.html +540 -0
- package/dist/engineering-pa-workspace/src/api.mjs +151 -48
- package/package.json +1 -1
|
@@ -156,6 +156,19 @@ quietly widen the policy.
|
|
|
156
156
|
Wire the hook up in your OWN `~/.claude/settings.json` (per-machine operator
|
|
157
157
|
territory — this package never writes to it):
|
|
158
158
|
|
|
159
|
+
The command is the launcher the scaffold writes into a new instance's
|
|
160
|
+
`.claude/settings.json` (copy it from that file): it walks up from
|
|
161
|
+
`$CLAUDE_PROJECT_DIR` to the nearest installed
|
|
162
|
+
`node_modules/@stravica-ai/digital-operator-pa/bin/pa-coordinator-hook` and
|
|
163
|
+
needs only `node` on the hook PATH.
|
|
164
|
+
|
|
165
|
+
This launcher, like the other PreToolUse gate, has no stdout latch. The
|
|
166
|
+
scaffold's SessionStart launchers do: once their bin has written a byte to
|
|
167
|
+
fd 1 they print nothing of their own. That latch counts only
|
|
168
|
+
`process.stdout.write` and `fs.writeSync` to fd 1, so a SessionStart bin that
|
|
169
|
+
writes with async `fs.write`, `fs.writev` or `fs.writeFileSync(1)` bypasses
|
|
170
|
+
it. No shipped bin writes that way.
|
|
171
|
+
|
|
159
172
|
```jsonc
|
|
160
173
|
{
|
|
161
174
|
"hooks": {
|
|
@@ -165,7 +178,7 @@ territory — this package never writes to it):
|
|
|
165
178
|
"hooks": [
|
|
166
179
|
{
|
|
167
180
|
"type": "command",
|
|
168
|
-
"command": "pa-coordinator-hook"
|
|
181
|
+
"command": "node -e 'var P=require(\"path\"),d=process.env.CLAUDE_PROJECT_DIR,q=\"node_modules/@stravica-ai/digital-operator-pa/bin/pa-coordinator-hook\",y=e=>x(e?.message||e),x=r=>process.exit(console.error(\"pa-coordinator-hook could not run: \"+r)|1);for(var c=P.resolve(d||x(\"no CLAUDE_PROJECT_DIR\")),b,n;!require(\"fs\").existsSync(b=P.join(c,q));c=n)if(c==(n=P.dirname(c)))x(\"no \"+q+\" above \"+d);process.argv.splice(1,0,b);import(require(\"url\").pathToFileURL(b)).catch(e=>process.listenerCount(\"uncaughtException\")?process.nextTick(()=>{throw e}):y(e))'"
|
|
169
182
|
}
|
|
170
183
|
]
|
|
171
184
|
}
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Cache: <repo-root>/.session/agent-audit.json.
|
|
20
20
|
// Staleness: 60 minutes, override via operator/config/preferences.json:
|
|
21
|
-
// agentAudit.cacheStalenessMinutes.
|
|
21
|
+
// agentAudit.cacheStalenessMinutes. A cache whose manifestVersion
|
|
22
|
+
// differs from the bundled manifest's (or that has none) is stale
|
|
23
|
+
// at any age, so a package update shows new capabilities at once.
|
|
22
24
|
//
|
|
23
25
|
// Performance target: cache-read <2s (session-boot budget), refresh <3s
|
|
24
26
|
// on a real estate. All probes are Node-only synchronous, so the whole
|
|
@@ -52,7 +54,8 @@ Global flags:
|
|
|
52
54
|
|
|
53
55
|
Cache file: <repo-root>/.session/agent-audit.json.
|
|
54
56
|
Staleness threshold: 60 minutes (override via operator/config/preferences.json:
|
|
55
|
-
agentAudit.cacheStalenessMinutes).
|
|
57
|
+
agentAudit.cacheStalenessMinutes). A cache written under another manifest
|
|
58
|
+
version is rebuilt at any age.
|
|
56
59
|
|
|
57
60
|
Exit codes (tooling standard §6):
|
|
58
61
|
0 ok | 1 usage | 2 unresolvable state | 2 --strict fail
|
|
@@ -129,11 +132,12 @@ async function runAudit({ flags, forceRefresh }) {
|
|
|
129
132
|
err.exitCode = 2;
|
|
130
133
|
throw err;
|
|
131
134
|
}
|
|
135
|
+
const manifest = loadBundledManifestSync();
|
|
132
136
|
const cached = forceRefresh ? null : readCache(cachePath);
|
|
133
|
-
const stale = cached ? isStale(cached, staleness) : true;
|
|
137
|
+
const stale = cached ? isStale(cached, staleness, manifest.manifestVersion) : true;
|
|
134
138
|
let audit;
|
|
135
139
|
if (!cached || stale) {
|
|
136
|
-
audit = performAudit({ repoRoot, paName });
|
|
140
|
+
audit = performAudit({ repoRoot, paName, manifest });
|
|
137
141
|
writeCache(cachePath, audit);
|
|
138
142
|
} else {
|
|
139
143
|
audit = cached;
|
|
@@ -153,8 +157,8 @@ async function runAudit({ flags, forceRefresh }) {
|
|
|
153
157
|
* Perform a fresh audit — pure orchestration around the manifest loader
|
|
154
158
|
* and the audit runner. Exported for the boot-context hook and tests.
|
|
155
159
|
*/
|
|
156
|
-
export function performAudit({ repoRoot, paName, now, hostname }) {
|
|
157
|
-
|
|
160
|
+
export function performAudit({ repoRoot, paName, now, hostname, manifest }) {
|
|
161
|
+
manifest = manifest ?? loadBundledManifestSync();
|
|
158
162
|
const ctx = {
|
|
159
163
|
repoRoot,
|
|
160
164
|
paName,
|
|
@@ -192,11 +196,17 @@ function writeCache(path, cache) {
|
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
198
|
|
|
195
|
-
|
|
199
|
+
/**
|
|
200
|
+
* A cache is stale when it was written under another manifest version (or
|
|
201
|
+
* carries none), so an upgraded package never serves the old capability
|
|
202
|
+
* list, or when it is older than the staleness threshold.
|
|
203
|
+
*/
|
|
204
|
+
export function isStale(cache, stalenessMinutes, manifestVersion, nowMs = Date.now()) {
|
|
205
|
+
if (cache?.manifestVersion === undefined || cache.manifestVersion !== manifestVersion) return true;
|
|
196
206
|
if (!cache?.capturedAt) return true;
|
|
197
207
|
const capturedMs = Date.parse(cache.capturedAt);
|
|
198
208
|
if (!Number.isFinite(capturedMs)) return true;
|
|
199
|
-
const ageMs =
|
|
209
|
+
const ageMs = nowMs - capturedMs;
|
|
200
210
|
return ageMs > stalenessMinutes * 60 * 1000;
|
|
201
211
|
}
|
|
202
212
|
|
|
@@ -33,6 +33,8 @@ export function runProbe(probe, ctx) {
|
|
|
33
33
|
case 'knowledge-doc-exists': return probeKnowledgeDocExists(probe, ctx);
|
|
34
34
|
case 'schedule-defined': return probeScheduleDefined(probe, ctx);
|
|
35
35
|
case 'json-field-includes': return probeJsonFieldIncludes(probe, ctx);
|
|
36
|
+
case 'settings-hook-includes': return probeSettingsHookIncludes(probe, ctx);
|
|
37
|
+
case 'file-includes': return probeFileIncludes(probe, ctx);
|
|
36
38
|
default:
|
|
37
39
|
return {
|
|
38
40
|
outcome: UNVERIFIED,
|
|
@@ -234,3 +236,95 @@ function probeJsonFieldIncludes(probe, ctx) {
|
|
|
234
236
|
}
|
|
235
237
|
return { outcome: PASS, detail: `${rel}: '${field}' includes ${JSON.stringify(value)}` };
|
|
236
238
|
}
|
|
239
|
+
|
|
240
|
+
function probeSettingsHookIncludes(probe, ctx) {
|
|
241
|
+
// Reads every candidate that exists: `path` (the scaffold writes
|
|
242
|
+
// `.claude/settings.json` at the repo root) and each `altPaths` entry
|
|
243
|
+
// (an agent-folder settings file). A session may load only one of them,
|
|
244
|
+
// so the probe passes when any candidate holds the hook: some entry under
|
|
245
|
+
// `hooks.<event>` with a matcher containing `matcherIncludes` and a
|
|
246
|
+
// command hook whose command contains every `commandIncludes` string.
|
|
247
|
+
// A candidate that is not valid JSON (or cannot be read) does not stop
|
|
248
|
+
// the search. With no pass, the result is unverified-here when some
|
|
249
|
+
// candidate was invalid, else fail; the detail names each candidate read.
|
|
250
|
+
// No candidate at all fails.
|
|
251
|
+
const candidates = [...new Set([probe.path, ...(Array.isArray(probe.altPaths) ? probe.altPaths : [])]
|
|
252
|
+
.filter((p) => typeof p === 'string' && p.length > 0)
|
|
253
|
+
.map((p) => expandTemplate(p, ctx)))];
|
|
254
|
+
const present = candidates.filter((r) => existsSync(join(ctx.repoRoot, r)));
|
|
255
|
+
if (!present.length) {
|
|
256
|
+
return { outcome: FAIL, detail: `settings file missing: ${candidates.join(' or ')}` };
|
|
257
|
+
}
|
|
258
|
+
const misses = [];
|
|
259
|
+
let invalid = false;
|
|
260
|
+
for (const rel of present) {
|
|
261
|
+
const r = settingsFileHook(rel, probe, ctx);
|
|
262
|
+
if (r.pass) return { outcome: PASS, detail: r.detail };
|
|
263
|
+
if (r.invalid) invalid = true;
|
|
264
|
+
misses.push(r.detail);
|
|
265
|
+
}
|
|
266
|
+
return { outcome: invalid ? UNVERIFIED : FAIL, detail: misses.join('; ') };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** One settings file: { pass, invalid, detail }. Never throws. */
|
|
270
|
+
function settingsFileHook(rel, probe, ctx) {
|
|
271
|
+
let text;
|
|
272
|
+
try {
|
|
273
|
+
text = readFileSync(join(ctx.repoRoot, rel), 'utf8');
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return { pass: false, invalid: true, detail: `${rel} could not be read: ${err.message}` };
|
|
276
|
+
}
|
|
277
|
+
let doc;
|
|
278
|
+
try {
|
|
279
|
+
doc = JSON.parse(text);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return { pass: false, invalid: true, detail: `${rel} not valid JSON: ${err.message}` };
|
|
282
|
+
}
|
|
283
|
+
const event = probe.event;
|
|
284
|
+
const wants = Array.isArray(probe.commandIncludes) ? probe.commandIncludes : [];
|
|
285
|
+
const matcherWant = typeof probe.matcherIncludes === 'string' ? probe.matcherIncludes : null;
|
|
286
|
+
const entries = doc?.hooks?.[event];
|
|
287
|
+
if (!Array.isArray(entries)) {
|
|
288
|
+
return { pass: false, invalid: false, detail: `${rel}: no hooks.${event} entries` };
|
|
289
|
+
}
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
292
|
+
const matcher = typeof entry.matcher === 'string' ? entry.matcher : '';
|
|
293
|
+
if (matcherWant !== null && !matcher.includes(matcherWant)) continue;
|
|
294
|
+
for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
|
|
295
|
+
if (!hook || hook.type !== 'command' || typeof hook.command !== 'string') continue;
|
|
296
|
+
if (wants.every((w) => hook.command.includes(w))) {
|
|
297
|
+
return { pass: true, invalid: false, detail: `${rel}: hooks.${event} matcher ${JSON.stringify(matcher)} runs ${JSON.stringify(hook.command)}` };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const matcherText = matcherWant === null ? '' : ` with a matcher containing ${JSON.stringify(matcherWant)}`;
|
|
302
|
+
return {
|
|
303
|
+
pass: false,
|
|
304
|
+
invalid: false,
|
|
305
|
+
detail: `${rel}: no hooks.${event} entry${matcherText} runs a command containing ${JSON.stringify(wants)}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function probeFileIncludes(probe, ctx) {
|
|
310
|
+
const rel = expandTemplate(probe.path, ctx);
|
|
311
|
+
const abs = join(ctx.repoRoot, rel);
|
|
312
|
+
if (!existsSync(abs)) {
|
|
313
|
+
return { outcome: FAIL, detail: `missing: ${rel}` };
|
|
314
|
+
}
|
|
315
|
+
let text;
|
|
316
|
+
try {
|
|
317
|
+
if (!statSync(abs).isFile()) {
|
|
318
|
+
return { outcome: FAIL, detail: `path exists but is not a file: ${rel}` };
|
|
319
|
+
}
|
|
320
|
+
text = readFileSync(abs, 'utf8');
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return { outcome: UNVERIFIED, detail: `read failed for ${rel}: ${err.message}` };
|
|
323
|
+
}
|
|
324
|
+
const wants = Array.isArray(probe.includes) ? probe.includes : [];
|
|
325
|
+
const missing = wants.filter((w) => !text.includes(w));
|
|
326
|
+
if (missing.length) {
|
|
327
|
+
return { outcome: FAIL, detail: `${rel} does not include ${JSON.stringify(missing)}` };
|
|
328
|
+
}
|
|
329
|
+
return { outcome: PASS, detail: `${rel} includes ${JSON.stringify(wants)}` };
|
|
330
|
+
}
|