@gtrabanco/pi-agentic-workflow 4.10.0 → 5.0.0-canary.20261104.1
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 +15 -10
- package/dist/config/defaults.js +3 -8
- package/dist/config/merge.js +5 -20
- package/dist/config/schema.js +26 -98
- package/dist/config/types.d.ts +12 -59
- package/dist/extension/factory.d.ts +3 -13
- package/dist/extension/factory.js +1 -5
- package/dist/extension/index.js +18 -26
- package/dist/extension/telemetry-wiring.d.ts +67 -0
- package/dist/extension/telemetry-wiring.js +152 -0
- package/dist/session-telemetry.d.ts +104 -0
- package/dist/session-telemetry.js +161 -0
- package/dist/settings/console.js +1 -0
- package/dist/telemetry-sinks.d.ts +39 -0
- package/dist/telemetry-sinks.js +97 -0
- package/package.json +5 -5
- package/skills/audit-docs/SKILL.md +14 -3
- package/skills/audit-pr/references/03_AUDIT_PROCESS.md +10 -6
- package/skills/execute-phase/SKILL.md +10 -5
- package/skills/execute-phase/references/PREFLIGHT.md +2 -2
- package/skills/execute-phase/references/risk-tiers.md +105 -0
- package/skills/fold-findings/SKILL.md +12 -10
- package/skills/init-workspace/SKILL.md +18 -10
- package/skills/init-workspace/references/BOOTSTRAP_DISCOVERY.md +49 -9
- package/skills/init-workspace/references/BOOTSTRAP_WRITE.md +47 -20
- package/skills/init-workspace/references/SERENA.md +24 -0
- package/skills/init-workspace/references/UPGRADE.md +46 -19
- package/skills/phase-contract/SKILL.md +1 -1
- package/skills/pre-execution-review/references/LEDGERS.md +8 -4
- package/skills/pre-execution-review/references/POLICY.md +2 -2
- package/skills/product-audit/SKILL.md +9 -4
- package/skills/product-audit/references/AUDIT_DIMENSIONS.md +2 -0
- package/skills/product-audit/references/AUDIT_PROCESS.md +5 -2
- package/skills/review-a11y/SKILL.md +11 -6
- package/skills/review-brand/SKILL.md +11 -6
- package/skills/review-change/SKILL.md +1 -1
- package/skills/review-change/references/OUTPUT_AND_GUARDRAILS.md +8 -7
- package/skills/review-change/references/PERSIST_AND_DECIDE.md +15 -12
- package/skills/review-code/SKILL.md +26 -6
- package/skills/review-debt/SKILL.md +10 -5
- package/skills/review-design/SKILL.md +10 -5
- package/skills/review-implementation/references/CLASSIFY.md +94 -42
- package/skills/review-perf/SKILL.md +13 -8
- package/skills/review-security/SKILL.md +17 -7
- package/skills/review-seo/SKILL.md +10 -5
- package/skills/review-verify/SKILL.md +10 -5
- package/skills/triage-issue/SKILL.md +3 -2
- package/skills/triage-issue/references/FOLD_LEDGER.md +6 -5
- package/skills/unit-lane/SKILL.md +37 -4
- package/skills/unit-lane/references/DOCS.md +15 -3
- package/skills/unit-lane/references/IMPLEMENT.md +4 -1
- package/skills/unit-lane/references/RELEASE.md +4 -4
- package/skills/unit-lane/references/TESTS.md +12 -1
- package/skills/workflow-status/SKILL.md +2 -2
- package/skills/workflow-status/references/SENSOR_CORE.md +33 -4
- package/dist/extension/ci-guard.d.ts +0 -29
- package/dist/extension/ci-guard.js +0 -39
- package/dist/extension/cost-guard.d.ts +0 -30
- package/dist/extension/cost-guard.js +0 -50
- package/dist/extension/guard-util.d.ts +0 -26
- package/dist/extension/guard-util.js +0 -35
- package/dist/extension/history-guard.d.ts +0 -59
- package/dist/extension/history-guard.js +0 -132
- package/dist/extension/merge-guard.d.ts +0 -36
- package/dist/extension/merge-guard.js +0 -79
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Two JSON files, both optional:
|
|
|
64
64
|
{
|
|
65
65
|
"default": { "model": "anthropic/claude-opus-4-5", "thinking": "high" },
|
|
66
66
|
"commands": {
|
|
67
|
-
"
|
|
67
|
+
"unit-lane": { "model": ["anthropic/claude-sonnet-4-5", "openai/gpt-5.2"], "thinking": "medium" },
|
|
68
68
|
"review-change": { "thinking": "max" }
|
|
69
69
|
},
|
|
70
70
|
"onUnavailableRoute": "stop",
|
|
@@ -84,7 +84,7 @@ whatever the session already had, because the shipped default route is
|
|
|
84
84
|
resolves and has credentials, without touching the session while probing; when
|
|
85
85
|
every entry is unusable the command stops (or, with `onUnavailableRoute`
|
|
86
86
|
`inherit`, warns and runs on the current model), naming each candidate and why
|
|
87
|
-
it was skipped. `
|
|
87
|
+
it was skipped. `unit-lane` above tries `anthropic/claude-sonnet-4-5` first,
|
|
88
88
|
then `openai/gpt-5.2`.
|
|
89
89
|
- `thinking` is one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`,
|
|
90
90
|
or `"inherit"`.
|
|
@@ -263,8 +263,8 @@ nothing runs on a model you did not pick. To run anyway on the current model, se
|
|
|
263
263
|
## Your session after a command
|
|
264
264
|
|
|
265
265
|
By default (**`onSettle: "keep"`**), the routed model and thinking level **stay**
|
|
266
|
-
in the open chat window once the command settles. If `
|
|
267
|
-
`glm-5` and you want to tweak the
|
|
266
|
+
in the open chat window once the command settles. If `unit-lane` runs on
|
|
267
|
+
`glm-5` and you want to tweak the SPEC it produced, your next prompt keeps
|
|
268
268
|
running on `glm-5` — same for a follow-up question. When you don't need the
|
|
269
269
|
heavy model, switch it yourself with `/model` (or Ctrl+P) to something cheaper;
|
|
270
270
|
nothing restores over your choice.
|
|
@@ -313,7 +313,7 @@ dialog.
|
|
|
313
313
|
|
|
314
314
|
| You see | Means |
|
|
315
315
|
| --- | --- |
|
|
316
|
-
| `refused: invalid configuration` | A config file was rejected. The same message names the field, e.g. `$.commands.
|
|
316
|
+
| `refused: invalid configuration` | A config file was rejected. The same message names the field, e.g. `$.commands.unit-lane.model`. Run `/agentic-workflow-settings` to see the file, or fix the JSON. |
|
|
317
317
|
| `stopped: the configured model` … `is not in the model registry` | The reference is wrong or the provider is not configured. Use `/model` to see the exact `provider/modelId`. |
|
|
318
318
|
| `has no configured credentials` | The model exists but you cannot use it yet. Authenticate, or set `onUnavailableRoute` to `inherit`. |
|
|
319
319
|
| `could not be selected` | Pi refused the switch. The command stops with the reason — unless `onUnavailableRoute` is `inherit`, in which case it warns and runs on your current model. |
|
|
@@ -324,11 +324,16 @@ dialog.
|
|
|
324
324
|
|
|
325
325
|
## Notes
|
|
326
326
|
|
|
327
|
-
- Verified against the peer floor Pi
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
327
|
+
- Verified against the peer floor Pi 1.1.0 (2026-11-03): the session telemetry
|
|
328
|
+
reads `tool_execution_end.durationMs`, which 1.0.4's published types do not
|
|
329
|
+
carry and 1.1.0's do (`npm pack @earendil-works/pi-coding-agent@1.0.4` and
|
|
330
|
+
`@1.1.0`, then `dist/core/extensions/types.d.ts` — 0 vs 2 occurrences). The
|
|
331
|
+
pre-telemetry surface was verified behaviourally at Pi 0.99.1 (2026-09-29)
|
|
332
|
+
(`pi install`, package skills, friendly command registration, routed set/clear,
|
|
333
|
+
settings console round-trip, `sendUserMessage` with prompt template expansion).
|
|
334
|
+
Also verified against Pi 1.5.0 (2026-11-02) via a local two-leg `tsc --noEmit`
|
|
335
|
+
run (legs `0.99.1` + `latest`, the `latest` leg resolving to pi 1.5.0;
|
|
336
|
+
COMPAT-OK); continuously against the
|
|
332
337
|
`latest` dist-tag by the `pi-compat-guard` PR gate (the leg is a moving tripwire,
|
|
333
338
|
not a pinned version — a breaking release shows red here first).
|
|
334
339
|
- The package declares Pi as a peer dependency; it bundles no copy of Pi.
|
package/dist/config/defaults.js
CHANGED
|
@@ -20,14 +20,9 @@ export const DEFAULT_CONFIG = {
|
|
|
20
20
|
// Feature 66 — the memory substrate is ask-first: nothing is provisioned
|
|
21
21
|
// until the operator opts in.
|
|
22
22
|
memory: { engram: false },
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
merge: false,
|
|
27
|
-
history: false,
|
|
28
|
-
ci: { create: false, run: false },
|
|
29
|
-
expensiveTests: { maxTokens: 0, patterns: [] },
|
|
30
|
-
},
|
|
23
|
+
// Issue #383 — the measured acceptance is opt-in: a fresh install measures
|
|
24
|
+
// nothing until an operator turns it on.
|
|
25
|
+
telemetry: { enabled: false },
|
|
31
26
|
profiles: {},
|
|
32
27
|
profileOrder: [],
|
|
33
28
|
profileFallback: { ...DEFAULT_PROFILE_FALLBACK },
|
package/dist/config/merge.js
CHANGED
|
@@ -132,9 +132,11 @@ export function mergeConfigs(globalFile = {}, projectFile = {}) {
|
|
|
132
132
|
memory: {
|
|
133
133
|
engram: pick(false, projectFile.memory?.engram, globalFile.memory?.engram) === true,
|
|
134
134
|
},
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
135
|
+
// Issue #383 — the same boolean resolution: project, then global, then the
|
|
136
|
+
// shipped "no".
|
|
137
|
+
telemetry: {
|
|
138
|
+
enabled: pick(false, projectFile.telemetry?.enabled, globalFile.telemetry?.enabled) === true,
|
|
139
|
+
},
|
|
138
140
|
profiles: mergeProfiles(globalFile.profiles, projectFile.profiles),
|
|
139
141
|
profileOrder: pick([], projectFile.profileOrder, globalFile.profileOrder),
|
|
140
142
|
profileFallback: mergeProfileFallback(globalFile.profileFallback, projectFile.profileFallback),
|
|
@@ -147,23 +149,6 @@ export function mergeConfigs(globalFile = {}, projectFile = {}) {
|
|
|
147
149
|
merged.advance = advance;
|
|
148
150
|
return merged;
|
|
149
151
|
}
|
|
150
|
-
/** Guards merge whole-knob: a boolean takes project-then-global, and the
|
|
151
|
-
* expensive-tests block resolves field-by-field (patterns are replaced, not
|
|
152
|
-
* concatenated, so a project override is predictable — no hidden union). */
|
|
153
|
-
function mergeGuards(globalGuards, projectGuards) {
|
|
154
|
-
return {
|
|
155
|
-
merge: pick(DEFAULT_CONFIG.guards.merge, projectGuards?.merge, globalGuards?.merge),
|
|
156
|
-
history: pick(DEFAULT_CONFIG.guards.history, projectGuards?.history, globalGuards?.history),
|
|
157
|
-
ci: {
|
|
158
|
-
create: pick(DEFAULT_CONFIG.guards.ci.create, projectGuards?.ci?.create, globalGuards?.ci?.create),
|
|
159
|
-
run: pick(DEFAULT_CONFIG.guards.ci.run, projectGuards?.ci?.run, globalGuards?.ci?.run),
|
|
160
|
-
},
|
|
161
|
-
expensiveTests: {
|
|
162
|
-
maxTokens: pick(DEFAULT_CONFIG.guards.expensiveTests.maxTokens, projectGuards?.expensiveTests?.maxTokens, globalGuards?.expensiveTests?.maxTokens),
|
|
163
|
-
patterns: pick(DEFAULT_CONFIG.guards.expensiveTests.patterns, projectGuards?.expensiveTests?.patterns, globalGuards?.expensiveTests?.patterns),
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
152
|
/** Project-over-global merge of the advance (conductor) knobs (feature 62).
|
|
168
153
|
* Absent from the result when neither scope declares any — the resolved
|
|
169
154
|
* shape of pre-62 configs is byte-stable. */
|
package/dist/config/schema.js
CHANGED
|
@@ -21,7 +21,7 @@ const ROOT_KEYS = new Set([
|
|
|
21
21
|
"profileOrder",
|
|
22
22
|
"profileFallback",
|
|
23
23
|
"memory",
|
|
24
|
-
"
|
|
24
|
+
"telemetry",
|
|
25
25
|
]);
|
|
26
26
|
const ROUTE_KEYS = new Set(["model", "thinking"]);
|
|
27
27
|
const COMMAND_NAME = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
@@ -333,7 +333,7 @@ function validateConfig(value) {
|
|
|
333
333
|
if (!ROOT_KEYS.has(key)) {
|
|
334
334
|
issues.push({
|
|
335
335
|
path: `$.${displayKey(key)}`,
|
|
336
|
-
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance, recommendedModels, profiles, profileOrder, profileFallback, memory,
|
|
336
|
+
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute, onSettle, pathProtection, advance, recommendedModels, profiles, profileOrder, profileFallback, memory, telemetry)`,
|
|
337
337
|
});
|
|
338
338
|
}
|
|
339
339
|
}
|
|
@@ -403,16 +403,16 @@ function validateConfig(value) {
|
|
|
403
403
|
config.recommendedModels = value.recommendedModels;
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
|
+
if (value.telemetry !== undefined) {
|
|
407
|
+
const telemetry = checkTelemetry(value.telemetry, "$.telemetry", issues);
|
|
408
|
+
if (telemetry)
|
|
409
|
+
config.telemetry = telemetry;
|
|
410
|
+
}
|
|
406
411
|
if (value.memory !== undefined) {
|
|
407
412
|
const memory = checkMemory(value.memory, "$.memory", issues);
|
|
408
413
|
if (memory)
|
|
409
414
|
config.memory = memory;
|
|
410
415
|
}
|
|
411
|
-
if (value.guards !== undefined) {
|
|
412
|
-
const guards = checkGuards(value.guards, "$.guards", issues);
|
|
413
|
-
if (guards)
|
|
414
|
-
config.guards = guards;
|
|
415
|
-
}
|
|
416
416
|
if (value.profiles !== undefined) {
|
|
417
417
|
if (!isRecord(value.profiles)) {
|
|
418
418
|
issues.push({ path: "$.profiles", message: "must be an object mapping profile names to profile definitions" });
|
|
@@ -448,125 +448,53 @@ function validateConfig(value) {
|
|
|
448
448
|
* the loader can treat "nothing declared" like "nothing present" without a
|
|
449
449
|
* second read.
|
|
450
450
|
*/
|
|
451
|
-
/** Strict validator for the `
|
|
452
|
-
function
|
|
451
|
+
/** Strict validator for the `telemetry` block (issue #383): one boolean switch. */
|
|
452
|
+
function checkTelemetry(value, path, issues) {
|
|
453
453
|
if (!isRecord(value)) {
|
|
454
454
|
issues.push({ path, message: `must be a JSON object, got ${describe(value)}` });
|
|
455
455
|
return undefined;
|
|
456
456
|
}
|
|
457
457
|
let unknown = false;
|
|
458
458
|
for (const key of Object.keys(value)) {
|
|
459
|
-
if (key !== "
|
|
459
|
+
if (key !== "enabled") {
|
|
460
460
|
unknown = true;
|
|
461
|
-
issues.push({ path: `${path}.${displayKey(key)}`, message: `unknown
|
|
461
|
+
issues.push({ path: `${path}.${displayKey(key)}`, message: `unknown telemetry key "${key}" (allowed: enabled)` });
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
const out = {};
|
|
465
|
-
if (value.
|
|
466
|
-
if (typeof value.
|
|
467
|
-
issues.push({ path: `${path}.
|
|
465
|
+
if (value.enabled !== undefined) {
|
|
466
|
+
if (typeof value.enabled !== "boolean") {
|
|
467
|
+
issues.push({ path: `${path}.enabled`, message: `must be a boolean, got ${describe(value.enabled)}` });
|
|
468
468
|
}
|
|
469
469
|
else {
|
|
470
|
-
out.
|
|
470
|
+
out.enabled = value.enabled;
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
return unknown ? undefined : out;
|
|
474
474
|
}
|
|
475
|
-
/** Strict validator for the `
|
|
476
|
-
function
|
|
475
|
+
/** Strict validator for the `memory` consent block (feature 66, AC3). */
|
|
476
|
+
function checkMemory(value, path, issues) {
|
|
477
477
|
if (!isRecord(value)) {
|
|
478
478
|
issues.push({ path, message: `must be a JSON object, got ${describe(value)}` });
|
|
479
479
|
return undefined;
|
|
480
480
|
}
|
|
481
|
-
|
|
482
|
-
for (const key of ["merge", "history"]) {
|
|
483
|
-
if (value[key] !== undefined) {
|
|
484
|
-
if (typeof value[key] !== "boolean") {
|
|
485
|
-
issues.push({ path: `${path}.${key}`, message: `must be a boolean, got ${describe(value[key])}` });
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
out[key] = value[key];
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (value.ci !== undefined) {
|
|
493
|
-
const ciPath = `${path}.ci`;
|
|
494
|
-
if (!isRecord(value.ci)) {
|
|
495
|
-
issues.push({ path: ciPath, message: `must be a JSON object, got ${describe(value.ci)}` });
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
const ci = {};
|
|
499
|
-
for (const key of ["create", "run"]) {
|
|
500
|
-
if (value.ci[key] !== undefined) {
|
|
501
|
-
if (typeof value.ci[key] !== "boolean") {
|
|
502
|
-
issues.push({ path: `${ciPath}.${key}`, message: `must be a boolean, got ${describe(value.ci[key])}` });
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
ci[key] = value.ci[key];
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
for (const key of Object.keys(value.ci)) {
|
|
510
|
-
if (key !== "create" && key !== "run") {
|
|
511
|
-
issues.push({ path: `${ciPath}.${displayKey(key)}`, message: `unknown ci key "${key}" (allowed: create, run)` });
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
out.ci = ci;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (value.expensiveTests !== undefined) {
|
|
518
|
-
out.expensiveTests = checkExpensiveTests(value.expensiveTests, `${path}.expensiveTests`, issues);
|
|
519
|
-
}
|
|
481
|
+
let unknown = false;
|
|
520
482
|
for (const key of Object.keys(value)) {
|
|
521
|
-
if (key !== "
|
|
522
|
-
|
|
483
|
+
if (key !== "engram") {
|
|
484
|
+
unknown = true;
|
|
485
|
+
issues.push({ path: `${path}.${displayKey(key)}`, message: `unknown memory key "${key}" (allowed: engram)` });
|
|
523
486
|
}
|
|
524
487
|
}
|
|
525
|
-
return out;
|
|
526
|
-
}
|
|
527
|
-
function checkExpensiveTests(value, path, issues) {
|
|
528
|
-
if (!isRecord(value)) {
|
|
529
|
-
issues.push({ path, message: `must be a JSON object, got ${describe(value)}` });
|
|
530
|
-
return undefined;
|
|
531
|
-
}
|
|
532
488
|
const out = {};
|
|
533
|
-
if (value.
|
|
534
|
-
if (typeof value.
|
|
535
|
-
issues.push({ path: `${path}.
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
out.maxTokens = value.maxTokens;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (value.patterns !== undefined) {
|
|
542
|
-
if (!Array.isArray(value.patterns)) {
|
|
543
|
-
issues.push({ path: `${path}.patterns`, message: `must be an array, got ${describe(value.patterns)}` });
|
|
489
|
+
if (value.engram !== undefined) {
|
|
490
|
+
if (typeof value.engram !== "boolean") {
|
|
491
|
+
issues.push({ path: `${path}.engram`, message: `must be a boolean, got ${describe(value.engram)}` });
|
|
544
492
|
}
|
|
545
493
|
else {
|
|
546
|
-
|
|
547
|
-
value.patterns.forEach((entry, index) => {
|
|
548
|
-
const entryPath = `${path}.patterns[${index}]`;
|
|
549
|
-
if (!isRecord(entry) || typeof entry.match !== "string" || entry.match === "") {
|
|
550
|
-
issues.push({ path: entryPath, message: `must be an object with a non-empty string "match", got ${describe(entry)}` });
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (typeof entry.tokens !== "number" || !Number.isInteger(entry.tokens) || entry.tokens < 0) {
|
|
554
|
-
issues.push({ path: `${entryPath}.tokens`, message: `must be a non-negative integer, got ${describe(entry.tokens)}` });
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
try {
|
|
558
|
-
new RegExp(entry.match, "u");
|
|
559
|
-
}
|
|
560
|
-
catch {
|
|
561
|
-
issues.push({ path: `${entryPath}.match`, message: `not a valid regular expression: ${entry.match}` });
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
patterns.push({ match: entry.match, tokens: entry.tokens });
|
|
565
|
-
});
|
|
566
|
-
out.patterns = patterns;
|
|
494
|
+
out.engram = value.engram;
|
|
567
495
|
}
|
|
568
496
|
}
|
|
569
|
-
return out;
|
|
497
|
+
return unknown ? undefined : out;
|
|
570
498
|
}
|
|
571
499
|
export function parseConfigFile(text) {
|
|
572
500
|
if (text.trim() === "")
|
package/dist/config/types.d.ts
CHANGED
|
@@ -78,9 +78,8 @@ export interface ConfigFile {
|
|
|
78
78
|
/** Feature 66 (AC3/D3): the memory-substrate consent surface — Engram's MCP
|
|
79
79
|
* registration is ask-first, so nothing is provisioned unless this says so. */
|
|
80
80
|
memory?: MemoryConfigFile;
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
guards?: GuardsConfigFile;
|
|
81
|
+
/** Issue #383: the per-unit cost sample. */
|
|
82
|
+
telemetry?: TelemetryConfigFile;
|
|
84
83
|
/** Named user profiles keyed by profile name (feature 63, AC5). */
|
|
85
84
|
profiles?: Record<string, ProfileFile>;
|
|
86
85
|
/** Ordered list of profile names to probe; empty means ["default"] only
|
|
@@ -108,60 +107,14 @@ export interface MemoryConfigFile {
|
|
|
108
107
|
export interface MemoryConfig {
|
|
109
108
|
engram: boolean;
|
|
110
109
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
* a forced update, a ref delete or a push to `main` is
|
|
120
|
-
* declined — one mechanism covering every flag spelling and
|
|
121
|
-
* git alias.
|
|
122
|
-
* ci CI is owner-gated: creating workflow files and triggering
|
|
123
|
-
* workflow runs are blocked.
|
|
124
|
-
* expensiveTests commands matching a configured pattern are estimated in
|
|
125
|
-
* tokens; above `maxTokens` they are blocked. `tokens: 0`
|
|
126
|
-
* blocks every match regardless of estimate.
|
|
127
|
-
*
|
|
128
|
-
* The only way through a guard is the explicit per-command token — the
|
|
129
|
-
* command's first word — after an owner authorization in the same turn:
|
|
130
|
-
* `AW_ALLOW=merge`, `AW_ALLOW=force`, `AW_ALLOW=ci`, `AW_ALLOW=cost`.
|
|
131
|
-
*/
|
|
132
|
-
export interface GuardsConfigFile {
|
|
133
|
-
merge?: boolean;
|
|
134
|
-
history?: boolean;
|
|
135
|
-
ci?: GuardsCiFile;
|
|
136
|
-
expensiveTests?: GuardsExpensiveTestsFile;
|
|
137
|
-
}
|
|
138
|
-
export interface GuardsCiFile {
|
|
139
|
-
create?: boolean;
|
|
140
|
-
run?: boolean;
|
|
141
|
-
}
|
|
142
|
-
export interface GuardsExpensiveTestsFile {
|
|
143
|
-
/** Estimated-token ceiling; a match above it is blocked. `0` (default)
|
|
144
|
-
* blocks every match regardless of estimate. */
|
|
145
|
-
maxTokens?: number;
|
|
146
|
-
/** Command patterns (regex source, `u` flag) and their estimated cost. */
|
|
147
|
-
patterns?: GuardsCostPattern[];
|
|
148
|
-
}
|
|
149
|
-
export interface GuardsCostPattern {
|
|
150
|
-
match: string;
|
|
151
|
-
tokens: number;
|
|
152
|
-
}
|
|
153
|
-
/** What the extension READS: every guard knob resolved. */
|
|
154
|
-
export interface GuardsConfig {
|
|
155
|
-
merge: boolean;
|
|
156
|
-
history: boolean;
|
|
157
|
-
ci: {
|
|
158
|
-
create: boolean;
|
|
159
|
-
run: boolean;
|
|
160
|
-
};
|
|
161
|
-
expensiveTests: {
|
|
162
|
-
maxTokens: number;
|
|
163
|
-
patterns: GuardsCostPattern[];
|
|
164
|
-
};
|
|
110
|
+
/** What an operator WRITES under `telemetry` (issue #383). */
|
|
111
|
+
export interface TelemetryConfigFile {
|
|
112
|
+
/** Opt in to the per-unit cost sample. Absent or `false` measures nothing. */
|
|
113
|
+
enabled?: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** What the extension READS: the telemetry switch, resolved (issue #383). */
|
|
116
|
+
export interface TelemetryConfig {
|
|
117
|
+
enabled: boolean;
|
|
165
118
|
}
|
|
166
119
|
export interface ConfigIssue {
|
|
167
120
|
path: string;
|
|
@@ -191,8 +144,8 @@ export interface EffectiveConfig {
|
|
|
191
144
|
profileFallback: ProfileFallbackConfig;
|
|
192
145
|
/** Feature 66: the memory-substrate consent surface. */
|
|
193
146
|
memory: MemoryConfig;
|
|
194
|
-
/**
|
|
195
|
-
|
|
147
|
+
/** Issue #383: the measured acceptance is opt-in, like every other runtime cost. */
|
|
148
|
+
telemetry: TelemetryConfig;
|
|
196
149
|
/** Feature 63: declaredness of the implicit `default` profile. */
|
|
197
150
|
declared: {
|
|
198
151
|
default: RouteDeclaration;
|
|
@@ -2,10 +2,6 @@ import type { LoadedConfig } from "../config/load.js";
|
|
|
2
2
|
import type { Catalogue } from "../routing/catalogue.js";
|
|
3
3
|
import type { Router } from "../routing/dispatch.js";
|
|
4
4
|
import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
|
|
5
|
-
import { mergeGuard } from "./merge-guard.js";
|
|
6
|
-
import { historyGuard } from "./history-guard.js";
|
|
7
|
-
import { ciGuard } from "./ci-guard.js";
|
|
8
|
-
import { costGuard } from "./cost-guard.js";
|
|
9
5
|
import type { ExtensionSurface, InvocationContext, ModelRef, RoutingControls } from "../routing/types.js";
|
|
10
6
|
import type { HintStore } from "../routing/state.js";
|
|
11
7
|
/**
|
|
@@ -58,18 +54,12 @@ export interface ExtensionHandle<M extends ModelRef = ModelRef> {
|
|
|
58
54
|
catalogue: Catalogue;
|
|
59
55
|
/**
|
|
60
56
|
* The Pi-free predicates the adapter (`index.ts`) binds to Pi's lifecycle
|
|
61
|
-
* events: the inline-receipt block guard
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* entry stays a thin translation layer and the decisions are unit-tested with
|
|
65
|
-
* no session (issues #182, #278).
|
|
57
|
+
* events: the inline-receipt block guard and the dirty-worktree notice. They
|
|
58
|
+
* travel with the handle so the entry stays a thin translation layer and the
|
|
59
|
+
* decisions are unit-tested with no session (issue #182).
|
|
66
60
|
*/
|
|
67
61
|
guards: {
|
|
68
62
|
receiptGuard: typeof receiptGuard;
|
|
69
|
-
mergeGuard: typeof mergeGuard;
|
|
70
|
-
historyGuard: typeof historyGuard;
|
|
71
|
-
ciGuard: typeof ciGuard;
|
|
72
|
-
costGuard: typeof costGuard;
|
|
73
63
|
dirtyWorktreeWarning: typeof dirtyWorktreeWarning;
|
|
74
64
|
};
|
|
75
65
|
}
|
|
@@ -2,10 +2,6 @@ import { loadConfig } from "../config/load.js";
|
|
|
2
2
|
import { readCatalogue } from "../routing/catalogue.js";
|
|
3
3
|
import { createRouter } from "../routing/dispatch.js";
|
|
4
4
|
import { dirtyWorktreeWarning, receiptGuard } from "./receipt-guard.js";
|
|
5
|
-
import { mergeGuard } from "./merge-guard.js";
|
|
6
|
-
import { historyGuard } from "./history-guard.js";
|
|
7
|
-
import { ciGuard } from "./ci-guard.js";
|
|
8
|
-
import { costGuard } from "./cost-guard.js";
|
|
9
5
|
import { ADVANCE_COMMAND, SETTINGS_COMMAND, SETTINGS_COMMAND_ALIAS } from "../routing/types.js";
|
|
10
6
|
import { registerAdvanceCommand } from "./conductor-command.js";
|
|
11
7
|
import { createProfileStateStore, stateFilePath } from "../routing/state.js";
|
|
@@ -82,5 +78,5 @@ export function createExtension(deps) {
|
|
|
82
78
|
description: "Show and configure per-command model routing",
|
|
83
79
|
handler: settingsHandler,
|
|
84
80
|
});
|
|
85
|
-
return { router, catalogue, guards: { receiptGuard,
|
|
81
|
+
return { router, catalogue, guards: { receiptGuard, dirtyWorktreeWarning } };
|
|
86
82
|
}
|
package/dist/extension/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readGitStatusBounded } from "./receipt-guard.js";
|
|
|
9
9
|
import { createPickerComponent, PICKER_MAX_VISIBLE, pagedSelect } from "../settings/picker.js";
|
|
10
10
|
import { createHintStore, createSerenaReadinessStore, stateFilePath } from "../routing/state.js";
|
|
11
11
|
import { createSerenaReadinessGate, readInitWorkspaceRevision } from "../routing/serena-readiness.js";
|
|
12
|
+
import { createTelemetryWiring } from "./telemetry-wiring.js";
|
|
12
13
|
import { provisionEngram, readDeclaredServerNamesFrom, resolveEngramBinary, declaredMcpServerPaths } from "./engram.js";
|
|
13
14
|
import { runSettingsConsole } from "../settings/console.js";
|
|
14
15
|
import { readConfigFile, writeConfigFile } from "../settings/store.js";
|
|
@@ -89,11 +90,14 @@ function toInvocationContext(ctx) {
|
|
|
89
90
|
hasConfiguredAuth: (model) => ctx.modelRegistry.hasConfiguredAuth(model),
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
|
-
function toRegistrar(pi) {
|
|
93
|
+
function toRegistrar(pi, noteCommand) {
|
|
93
94
|
return {
|
|
94
95
|
registerCommand: (name, options) => pi.registerCommand(name, {
|
|
95
96
|
...(options.description ? { description: options.description } : {}),
|
|
96
97
|
handler: async (args, ctx) => {
|
|
98
|
+
// Issue #383: the session learns which unit it is working on from the
|
|
99
|
+
// command that names it, never from a model's claim.
|
|
100
|
+
noteCommand(name, String(args ?? "").split(/\s+/).filter(Boolean));
|
|
97
101
|
await options.handler(args, toInvocationContext(ctx));
|
|
98
102
|
},
|
|
99
103
|
}),
|
|
@@ -229,8 +233,13 @@ export default function extension(pi) {
|
|
|
229
233
|
// Said once per session: an override that was ignored (or a malformed file)
|
|
230
234
|
// is reported the first time the guard runs, never on every tool call.
|
|
231
235
|
let reportedDegradations = false;
|
|
236
|
+
// Issue #383 — the session accumulator. Pi builds a fresh extension instance
|
|
237
|
+
// per session (its runtime factory rebuilds the services, and the resource
|
|
238
|
+
// loader re-runs every factory), so one instance owns one session's sample.
|
|
239
|
+
let telemetry = null;
|
|
240
|
+
const noteCommand = (name, args) => telemetry?.noteCommand(name, args);
|
|
232
241
|
const { router, guards } = createExtension({
|
|
233
|
-
registrar: toRegistrar(pi),
|
|
242
|
+
registrar: toRegistrar(pi, noteCommand),
|
|
234
243
|
// Resolved per call: the router must never hold a session-bound object
|
|
235
244
|
// between turns, because Pi can hand a new session to the same extension.
|
|
236
245
|
surface: () => ({
|
|
@@ -276,6 +285,13 @@ export default function extension(pi) {
|
|
|
276
285
|
ctx.ui.notify(message, "warning");
|
|
277
286
|
const projectTrusted = ctx.isProjectTrusted();
|
|
278
287
|
const loaded = loadConfig({ agentDir, cwd: ctx.cwd, projectTrusted });
|
|
288
|
+
if (loaded.config.telemetry?.enabled === true && telemetry === null) {
|
|
289
|
+
// The id is read once, here: pi invalidates a captured context after the
|
|
290
|
+
// session is replaced, and one extension instance serves one session.
|
|
291
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
292
|
+
telemetry = createTelemetryWiring({ root: ctx.cwd, enabled: true, sessionId: () => sessionId });
|
|
293
|
+
telemetry.register(pi);
|
|
294
|
+
}
|
|
279
295
|
const report = provisionEngram({
|
|
280
296
|
consented: loaded.config.memory?.engram === true,
|
|
281
297
|
configInvalid: !loaded.ok,
|
|
@@ -296,32 +312,8 @@ export default function extension(pi) {
|
|
|
296
312
|
// names the script that proves the receipt landed (issue #182). The guard
|
|
297
313
|
// itself filters to `bash`, so every tool call is checked — nesting it under
|
|
298
314
|
// one tool name would make the block unreachable for the calls it exists for.
|
|
299
|
-
//
|
|
300
|
-
// Owner-gated tool guards, all config-driven (`guards` in the config files;
|
|
301
|
-
// every guard ships OFF — a project opts in per guard):
|
|
302
|
-
// merge `git merge`, `gh pr merge`, `git pull`'s merge form (issue #278).
|
|
303
|
-
// history pushes decided by git itself (`git push --dry-run`): a forced
|
|
304
|
-
// update, a ref delete or a push to `main` is declined.
|
|
305
|
-
// ci writing workflow definitions and triggering workflow runs.
|
|
306
|
-
// cost commands estimated above the configured token budget.
|
|
307
|
-
// The only way through any of them is the explicit per-command token
|
|
308
|
-
// (`AW_ALLOW=merge|force|ci|cost`) after an owner instruction in the turn.
|
|
309
315
|
pi.on("tool_call", (event, ctx) => {
|
|
310
316
|
const command = "command" in event.input && typeof event.input.command === "string" ? event.input.command : undefined;
|
|
311
|
-
const inputPath = "path" in event.input && typeof event.input.path === "string" ? event.input.path : undefined;
|
|
312
|
-
const { guards: policy } = loadConfig({ agentDir, cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() }).config;
|
|
313
|
-
const merge = guards.mergeGuard({ toolName: event.toolName, command }, policy.merge);
|
|
314
|
-
if (merge.block)
|
|
315
|
-
return merge;
|
|
316
|
-
const history = guards.historyGuard({ toolName: event.toolName, command }, policy.history, { cwd: ctx.cwd });
|
|
317
|
-
if (history.block)
|
|
318
|
-
return history;
|
|
319
|
-
const ci = guards.ciGuard({ toolName: event.toolName, command, path: inputPath }, policy.ci);
|
|
320
|
-
if (ci.block)
|
|
321
|
-
return ci;
|
|
322
|
-
const cost = guards.costGuard({ toolName: event.toolName, command }, policy.expensiveTests);
|
|
323
|
-
if (cost.block)
|
|
324
|
-
return cost;
|
|
325
317
|
const receipt = guards.receiptGuard({ toolName: event.toolName, command });
|
|
326
318
|
if (receipt.block)
|
|
327
319
|
return receipt;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetry-wiring.ts — attaches the session accumulator to Pi's lifecycle
|
|
3
|
+
* (issue #383 S4/S4b, P5).
|
|
4
|
+
*
|
|
5
|
+
* The accumulator and its sink are pure; this file is the only place that knows
|
|
6
|
+
* Pi exists. It is a factory over a `pi`-shaped object so the suite can drive the
|
|
7
|
+
* five events directly, and so `#394` can add a second sink without touching how
|
|
8
|
+
* the session is observed.
|
|
9
|
+
*
|
|
10
|
+
* Two decisions worth knowing when reading it:
|
|
11
|
+
*
|
|
12
|
+
* - **The unit comes from the command that was dispatched**, resolved against
|
|
13
|
+
* the project's own directories. A session that ran no unit-scoped command
|
|
14
|
+
* emits nothing: a sample without a unit would be a row the ledger cannot
|
|
15
|
+
* attribute, which is worse than no row.
|
|
16
|
+
* - **The sample names its session.** The sink's idempotency guard compares a
|
|
17
|
+
* whole row, so the session id in `source` is what keeps a second session of
|
|
18
|
+
* the same unit — the same day, the same tree — from being written over.
|
|
19
|
+
* - **Off registers no listener at all.** A listener that wakes up to find itself
|
|
20
|
+
* disabled is a cost with no feature, so `enabled: false` means `pi.on` is
|
|
21
|
+
* never called.
|
|
22
|
+
*/
|
|
23
|
+
import { type TelemetrySink } from "../session-telemetry.js";
|
|
24
|
+
/** The events this wiring listens to. */
|
|
25
|
+
export declare const TELEMETRY_EVENTS: readonly ["session_start", "turn_start", "turn_end", "tool_execution_end", "session_shutdown"];
|
|
26
|
+
/** The slice of `pi` the wiring needs. Pi hands every handler its context. */
|
|
27
|
+
export interface PiLike {
|
|
28
|
+
on(event: string, handler: (event?: unknown, ctx?: unknown) => void): void;
|
|
29
|
+
}
|
|
30
|
+
export interface TelemetryWiringOptions {
|
|
31
|
+
readonly root: string;
|
|
32
|
+
readonly enabled: boolean;
|
|
33
|
+
readonly sinks?: readonly TelemetrySink[];
|
|
34
|
+
/** Where the ledger lives, relative to `root`. */
|
|
35
|
+
readonly ledgerPath?: string;
|
|
36
|
+
/** How many paths this session changed; injected so the suite owns git too. */
|
|
37
|
+
readonly changedPaths?: () => number;
|
|
38
|
+
/** The HEAD the session started on, captured at `session_start`. */
|
|
39
|
+
readonly sessionStartSha?: () => string;
|
|
40
|
+
/**
|
|
41
|
+
* The id of the session being measured, so two sessions that touch the same
|
|
42
|
+
* unit on the same day are two samples instead of one. Read lazily: the
|
|
43
|
+
* caller already holds the live context when it builds the wiring.
|
|
44
|
+
*/
|
|
45
|
+
readonly sessionId?: () => string | undefined;
|
|
46
|
+
readonly now?: () => number;
|
|
47
|
+
readonly date?: () => string;
|
|
48
|
+
readonly source?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TelemetryWiring {
|
|
51
|
+
/** Note a dispatched command, so the session knows which unit it is working on. */
|
|
52
|
+
noteCommand(command: string, args: readonly string[]): void;
|
|
53
|
+
register(pi: PiLike): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How many distinct paths this session touched: the commits since the HEAD it
|
|
57
|
+
* started on, plus whatever is still uncommitted. It is the session's own scope —
|
|
58
|
+
* not the branch's — because the sample is the session's cost, and a long-lived
|
|
59
|
+
* branch would otherwise be charged to whichever unit happened to end it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function sessionChangedPaths(root: string, startSha: string): number;
|
|
62
|
+
/**
|
|
63
|
+
* Build the wiring. Everything it cannot know is injected: the project root, the
|
|
64
|
+
* sinks, the clock, the path count, the session id and the session's starting
|
|
65
|
+
* commit.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createTelemetryWiring(options: TelemetryWiringOptions): TelemetryWiring;
|