@event4u/agent-config 14.18.0 → 14.19.0
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +169 -130
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +420 -16
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +28 -27
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2132 -1462
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1619 -1045
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
|
@@ -113,6 +113,21 @@ export interface GateSpec {
|
|
|
113
113
|
* `pending` is reported instead of hidden.
|
|
114
114
|
*/
|
|
115
115
|
unavailable_exit?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Repo-relative workflow file this row's `argv` is pinned to.
|
|
118
|
+
*
|
|
119
|
+
* Rule 2 of this manifest — CI-IDENTICAL INVOCATION — was an instruction to
|
|
120
|
+
* the author and nothing else: the guard runs whatever `argv` says and never
|
|
121
|
+
* looked at how CI calls the gate, so a row could drift from its workflow, or
|
|
122
|
+
* the workflow step could be deleted outright, and this file would keep
|
|
123
|
+
* reporting healthy coverage for a gate CI no longer runs.
|
|
124
|
+
*
|
|
125
|
+
* Opt-in per row, because most gates here have several CI invocations with
|
|
126
|
+
* different arguments and one `argv` cannot be identical to all of them. Where
|
|
127
|
+
* a row DOES name a workflow, that workflow must contain an invocation of the
|
|
128
|
+
* gate whose argument list equals `argv` exactly.
|
|
129
|
+
*/
|
|
130
|
+
ci_invocation?: string;
|
|
116
131
|
note?: string;
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -191,6 +206,9 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
|
|
|
191
206
|
...(e['unavailable_exit'] === undefined
|
|
192
207
|
? {}
|
|
193
208
|
: { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
|
|
209
|
+
...(e['ci_invocation'] === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { ci_invocation: _require_str(e['ci_invocation'], 'ci_invocation', id, i) }),
|
|
194
212
|
...(e['note'] === undefined ? {} : { note: String(e['note']) }),
|
|
195
213
|
...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
|
|
196
214
|
...(e['no_canary_reason'] === undefined
|
|
@@ -405,6 +423,73 @@ function report_negative_control_inventory(specs: readonly GateSpec[]): void {
|
|
|
405
423
|
}
|
|
406
424
|
}
|
|
407
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Invocations of `src/scripts/<id>` in a workflow, each as its argument list.
|
|
428
|
+
*
|
|
429
|
+
* Deliberately a text scan rather than a YAML walk: an invocation lives inside a
|
|
430
|
+
* `run:` block scalar, so parsing the workflow buys nothing and costs the ability
|
|
431
|
+
* to see a call split across a line continuation, which is how the release job
|
|
432
|
+
* writes this very gate. Backslash-newline joins are folded before matching for
|
|
433
|
+
* that reason.
|
|
434
|
+
*/
|
|
435
|
+
export function ci_invocations(workflowText: string, id: string): string[][] {
|
|
436
|
+
const folded = workflowText.replace(/\\\n\s*/gu, ' ');
|
|
437
|
+
const out: string[][] = [];
|
|
438
|
+
const re = new RegExp(String.raw`src/scripts/${id}(?![A-Za-z0-9_])([^\n]*)`, 'gu');
|
|
439
|
+
for (const m of folded.matchAll(re)) {
|
|
440
|
+
out.push(
|
|
441
|
+
(m[1] ?? '')
|
|
442
|
+
.trim()
|
|
443
|
+
.split(/\s+/u)
|
|
444
|
+
.filter((t) => t !== ''),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The row's `argv` must be reproducible from the workflow it pins.
|
|
452
|
+
*
|
|
453
|
+
* Both directions are failures and they are different defects: no matching
|
|
454
|
+
* invocation means the manifest probes the gate in a way CI does not (or CI
|
|
455
|
+
* stopped calling it at all), which is the drift rule 2 asks for and nothing
|
|
456
|
+
* enforced.
|
|
457
|
+
*/
|
|
458
|
+
export function ci_invocation_problem(
|
|
459
|
+
workflowText: string,
|
|
460
|
+
id: string,
|
|
461
|
+
argv: readonly string[],
|
|
462
|
+
): string | null {
|
|
463
|
+
const found = ci_invocations(workflowText, id);
|
|
464
|
+
if (found.length === 0) {
|
|
465
|
+
return `no invocation of src/scripts/${id} — the workflow does not call this gate`;
|
|
466
|
+
}
|
|
467
|
+
const want = argv.join(' ');
|
|
468
|
+
if (found.some((a) => a.join(' ') === want)) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return (
|
|
472
|
+
`argv [${want}] matches no invocation there; the workflow calls it as ` +
|
|
473
|
+
found.map((a) => `[${a.join(' ')}]`).join(', ')
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Rows whose `ci_invocation` no longer describes their workflow. */
|
|
478
|
+
export function ci_invocation_drift(specs: readonly GateSpec[], repoRoot = REPO_ROOT): string[] {
|
|
479
|
+
const problems: string[] = [];
|
|
480
|
+
for (const s of specs) {
|
|
481
|
+
if (s.ci_invocation === undefined) continue;
|
|
482
|
+
const file = path.join(repoRoot, s.ci_invocation);
|
|
483
|
+
if (!fs.existsSync(file)) {
|
|
484
|
+
problems.push(`${s.id}: ci_invocation ${s.ci_invocation} does not exist`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const problem = ci_invocation_problem(fs.readFileSync(file, 'utf-8'), s.id, s.argv);
|
|
488
|
+
if (problem !== null) problems.push(`${s.id}: ${s.ci_invocation} — ${problem}`);
|
|
489
|
+
}
|
|
490
|
+
return problems;
|
|
491
|
+
}
|
|
492
|
+
|
|
408
493
|
export function main(argv: readonly string[]): number {
|
|
409
494
|
const wantJson = argv.includes('--format') && argv[argv.indexOf('--format') + 1] === 'json';
|
|
410
495
|
const listOnly = argv.includes('--list');
|
|
@@ -523,12 +608,23 @@ export function main(argv: readonly string[]): number {
|
|
|
523
608
|
);
|
|
524
609
|
}
|
|
525
610
|
report_negative_control_inventory(specs);
|
|
611
|
+
const drift = ci_invocation_drift(specs);
|
|
612
|
+
for (const d of drift) {
|
|
613
|
+
process.stdout.write(` ❌ ${d}\n`);
|
|
614
|
+
}
|
|
526
615
|
const hardening = report_hardening_ratchet();
|
|
527
616
|
const selfTest = report_self_test_ratchet();
|
|
528
617
|
if (failed.length > 0) {
|
|
529
618
|
process.stdout.write(`❌ ${String(failed.length)} gate(s) failed the coverage floor.\n`);
|
|
530
619
|
return 1;
|
|
531
620
|
}
|
|
621
|
+
if (drift.length > 0) {
|
|
622
|
+
process.stdout.write(
|
|
623
|
+
`❌ ${String(drift.length)} row(s) declare a ci_invocation their workflow no longer matches — ` +
|
|
624
|
+
`the manifest probes the gate differently from CI, or CI stopped calling it.\n`,
|
|
625
|
+
);
|
|
626
|
+
return 1;
|
|
627
|
+
}
|
|
532
628
|
if (hardening !== 0) {
|
|
533
629
|
return hardening;
|
|
534
630
|
}
|
|
@@ -72,13 +72,17 @@ import {
|
|
|
72
72
|
derive_categories,
|
|
73
73
|
parse_git_log,
|
|
74
74
|
mix_response_blockers,
|
|
75
|
+
previous_promise,
|
|
75
76
|
previous_release_tag,
|
|
77
|
+
promise_readback_blockers,
|
|
76
78
|
stale_draft_labels,
|
|
77
79
|
} from './_lib/release_highlights.js';
|
|
78
80
|
import { loadTaxonomy, measureRange } from './measure_release_mix.js';
|
|
79
81
|
import {
|
|
80
82
|
CURATED_HEAD_INSTRUCTION,
|
|
83
|
+
PROMISE_READBACK_MARKER,
|
|
81
84
|
extract_changelog_section,
|
|
85
|
+
previous_changelog_version,
|
|
82
86
|
} from './_lib/release_material.js';
|
|
83
87
|
import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
84
88
|
|
|
@@ -207,7 +211,8 @@ export function main(argv: readonly string[]): number {
|
|
|
207
211
|
}
|
|
208
212
|
throw err;
|
|
209
213
|
}
|
|
210
|
-
const
|
|
214
|
+
const changelogText = fs.readFileSync(changelogPath, 'utf-8');
|
|
215
|
+
const section = extract_changelog_section(changelogText, version);
|
|
211
216
|
if (!section) {
|
|
212
217
|
process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
|
|
213
218
|
return 2;
|
|
@@ -251,6 +256,11 @@ export function main(argv: readonly string[]): number {
|
|
|
251
256
|
return mix;
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
const readback = check_previous_promise_readback(changelogText, section.body, version);
|
|
260
|
+
if (readback !== 0) {
|
|
261
|
+
return readback;
|
|
262
|
+
}
|
|
263
|
+
|
|
254
264
|
const curated = parse_curated_head(section.body);
|
|
255
265
|
if (!curated) {
|
|
256
266
|
process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
|
|
@@ -417,6 +427,69 @@ export function check_governance_mix_response(
|
|
|
417
427
|
return 1;
|
|
418
428
|
}
|
|
419
429
|
|
|
430
|
+
/**
|
|
431
|
+
* The previous release's promise, read back at the next release.
|
|
432
|
+
*
|
|
433
|
+
* `_lib/release_material.ts` generates `Next cycle ships …` and, until this
|
|
434
|
+
* check existed, nothing ever asked what became of it: 14.18.0 promised the MCP
|
|
435
|
+
* bridge repair, and no gate in the tree would have noticed the same promise
|
|
436
|
+
* being reprinted in 14.19.0 and 14.20.0. A commitment nothing reads back is
|
|
437
|
+
* free to restate, and one that is free to restate is not a commitment.
|
|
438
|
+
*
|
|
439
|
+
* Only the section under release is read, and only the previous section's
|
|
440
|
+
* governance-response block — the same scope every other check here uses, so a
|
|
441
|
+
* historical section cannot red a future release.
|
|
442
|
+
*
|
|
443
|
+
* The check is about ANSWERING, never about having kept the promise. A gate
|
|
444
|
+
* that refused an unkept promise would price honesty above silence and teach
|
|
445
|
+
* the next author to promise nothing.
|
|
446
|
+
*/
|
|
447
|
+
export function check_previous_promise_readback(
|
|
448
|
+
changelogText: string,
|
|
449
|
+
body: string,
|
|
450
|
+
version: string,
|
|
451
|
+
): number {
|
|
452
|
+
const previousVersion = previous_changelog_version(changelogText, version);
|
|
453
|
+
if (previousVersion === null) {
|
|
454
|
+
process.stdout.write(`✅ no previous CHANGELOG section before ${version} — no promise to read back\n`);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
const promise = previous_promise(changelogText, previousVersion);
|
|
458
|
+
if (promise === null) {
|
|
459
|
+
process.stdout.write(
|
|
460
|
+
`✅ the ${previousVersion} section made no next-cycle promise — nothing owed by ${version}\n`,
|
|
461
|
+
);
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
const blockers = promise_readback_blockers(
|
|
465
|
+
body,
|
|
466
|
+
version,
|
|
467
|
+
previousVersion,
|
|
468
|
+
promise,
|
|
469
|
+
`\`release/${version}\``,
|
|
470
|
+
);
|
|
471
|
+
if (blockers.length === 0) {
|
|
472
|
+
process.stdout.write(`✅ ${version} answers the ${previousVersion} next-cycle promise\n`);
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
process.stderr.write(
|
|
476
|
+
`❌ the ${version} section owes an answer to the ${previousVersion} next-cycle promise.\n` +
|
|
477
|
+
blockers.map((b) => ` - ${b}\n`).join('') +
|
|
478
|
+
` The ${previousVersion} promise reads:\n` +
|
|
479
|
+
promise
|
|
480
|
+
.split('\n')
|
|
481
|
+
.map((l) => ` ${l}\n`)
|
|
482
|
+
.join('') +
|
|
483
|
+
` Answering it is one blockquote line under the curated head, e.g.\n` +
|
|
484
|
+
` > ${PROMISE_READBACK_MARKER} the <promise> did not ship; <what happened>.\n` +
|
|
485
|
+
' Shipped, did not ship, or withdrawn with a reason — the check refuses a MISSING\n' +
|
|
486
|
+
' answer, never a particular outcome. An unkept promise answered honestly passes.\n' +
|
|
487
|
+
' Reproduce locally, before any push:\n' +
|
|
488
|
+
` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
|
|
489
|
+
);
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
420
493
|
const _isMain = (() => {
|
|
421
494
|
const entry = process.argv[1];
|
|
422
495
|
if (!entry) return false;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The published skill-activation figure agrees with the record it came from.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A SEPARATE SCRIPT FROM THE CENSUS
|
|
6
|
+
* ---------------------------------------------
|
|
7
|
+
* `report_skill_activation` is a REPORT: it gates on nothing, has no threshold,
|
|
8
|
+
* and its own docstring says it must never acquire one. This is a GATE, and the
|
|
9
|
+
* two must not share an entry point — `src/config/gate-coverage.yml` is a
|
|
10
|
+
* manifest of gates, and `_lib/gate_population.ts` classifies a script by its
|
|
11
|
+
* prefix precisely so that "is this a gate" is answered structurally rather
|
|
12
|
+
* than by whoever last edited a config. Registering a `report_*` id there forks
|
|
13
|
+
* that population, which `tests/scripts/gate_population.test.ts` refuses.
|
|
14
|
+
*
|
|
15
|
+
* So the split is not cosmetic. The report keeps its exit-0 contract; the
|
|
16
|
+
* assertion that CAN fail lives here, where a floor and a self-test belong.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT CANNOT DO, STATED SO NOBODY READS IT AS COVERED
|
|
19
|
+
* ------------------------------------------------------
|
|
20
|
+
* It compares two COMMITTED files — the census record against the sentence
|
|
21
|
+
* `docs/CLAIMS.md` publishes. It cannot tell whether the record still describes
|
|
22
|
+
* the world: the transcript store it was taken from is one machine's, keyed on
|
|
23
|
+
* the directory the census ran in, and absent from CI. Re-taking the
|
|
24
|
+
* measurement is `report_skill_activation --emit`, which refuses to write from
|
|
25
|
+
* an empty store. This gate only forbids the published sentence and the record
|
|
26
|
+
* from telling two different stories.
|
|
27
|
+
*
|
|
28
|
+
* Exit: 0 agree · 1 disagree or a file is missing.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as os from 'node:os';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
37
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
38
|
+
import {
|
|
39
|
+
CLAIMS_PATH,
|
|
40
|
+
CLAIM_ID,
|
|
41
|
+
RECORD_PATH,
|
|
42
|
+
claimBlock,
|
|
43
|
+
claimProblems,
|
|
44
|
+
parseClaimFigures,
|
|
45
|
+
readRecord,
|
|
46
|
+
} from './report_skill_activation.js';
|
|
47
|
+
|
|
48
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
49
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
50
|
+
|
|
51
|
+
export function check(root: string): number {
|
|
52
|
+
const rec = readRecord(root);
|
|
53
|
+
if (rec === null) {
|
|
54
|
+
process.stderr.write(`❌ ${RECORD_PATH} is missing — the published figure has no record behind it.\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const claimsFile = path.join(root, CLAIMS_PATH);
|
|
58
|
+
if (!fs.existsSync(claimsFile)) {
|
|
59
|
+
process.stderr.write(`❌ ${CLAIMS_PATH} is missing.\n`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const block = claimBlock(fs.readFileSync(claimsFile, 'utf8'), CLAIM_ID);
|
|
63
|
+
// Emitted before the verdict on both paths: a gate that reports what it
|
|
64
|
+
// inspected only when it passes leaves the coverage census blind exactly when
|
|
65
|
+
// it matters.
|
|
66
|
+
reportScanned({
|
|
67
|
+
gate: 'check_skill_activation_claim',
|
|
68
|
+
scanned: block === null ? 0 : 1,
|
|
69
|
+
units: 'published claim block(s)',
|
|
70
|
+
roots: [CLAIMS_PATH],
|
|
71
|
+
});
|
|
72
|
+
if (block === null) {
|
|
73
|
+
process.stderr.write(`❌ claim \`${CLAIM_ID}\` is not in ${CLAIMS_PATH}.\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const problems = claimProblems(rec, parseClaimFigures(block));
|
|
77
|
+
if (problems.length > 0) {
|
|
78
|
+
process.stderr.write('❌ the published census claim and its record disagree:\n');
|
|
79
|
+
for (const p of problems) {
|
|
80
|
+
process.stderr.write(` · ${p}\n`);
|
|
81
|
+
}
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
`\nRe-take the measurement (report_skill_activation --emit) and update the claim from ${RECORD_PATH}.\n`,
|
|
84
|
+
);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`✅ census claim matches ${RECORD_PATH} (measured ${rec.measured_at}).\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The gate proving it still discriminates.
|
|
93
|
+
*
|
|
94
|
+
* The third case is the one a value comparison alone would miss: a claim that
|
|
95
|
+
* silently stops stating a figure would otherwise pass every equality test it
|
|
96
|
+
* no longer participates in.
|
|
97
|
+
*/
|
|
98
|
+
function selfTest(): number {
|
|
99
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'activation-claim-selftest-'));
|
|
100
|
+
const record = {
|
|
101
|
+
schema_version: 1,
|
|
102
|
+
measured_at: '2026-01-02',
|
|
103
|
+
skills_shipped: 300,
|
|
104
|
+
with_trigger_key: 10,
|
|
105
|
+
with_trigger_corpus: 100,
|
|
106
|
+
trigger_key_and_corpus: 5,
|
|
107
|
+
human_named_only: 195,
|
|
108
|
+
sessions: 7,
|
|
109
|
+
assistant_turns: 1234,
|
|
110
|
+
invocations: 0,
|
|
111
|
+
distinct_skills_invoked: 0,
|
|
112
|
+
stores: ['fixture'],
|
|
113
|
+
};
|
|
114
|
+
const agreeing =
|
|
115
|
+
'`report_skill_activation` over 7 sessions and 1,234 assistant turns records 0 Skill ' +
|
|
116
|
+
'invocations and 0 of 300 distinct skills (measured 2026-01-02). 10 declare a ' +
|
|
117
|
+
'machine-matchable trigger key in frontmatter, 100 carry an `evals/triggers.json` corpus, ' +
|
|
118
|
+
'5 do both, and the remaining 195 are reachable only by a human naming them.';
|
|
119
|
+
const write = (name: string, body: string): string => {
|
|
120
|
+
const root = path.join(tmp, name);
|
|
121
|
+
fs.mkdirSync(path.join(root, path.dirname(RECORD_PATH)), { recursive: true });
|
|
122
|
+
fs.mkdirSync(path.join(root, path.dirname(CLAIMS_PATH)), { recursive: true });
|
|
123
|
+
fs.writeFileSync(path.join(root, RECORD_PATH), JSON.stringify(record));
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(root, CLAIMS_PATH),
|
|
126
|
+
`# Claims\n\n### claim: ${CLAIM_ID}\n- claim: ${body}\n- kind: quant\n- status: backed\n- last_verified: 2026-01-02\n\n### claim: unrelated\n- claim: x\n`,
|
|
127
|
+
);
|
|
128
|
+
return root;
|
|
129
|
+
};
|
|
130
|
+
const run = (root: string): number =>
|
|
131
|
+
runGateCli(REPO_ROOT, 'src/scripts/check_skill_activation_claim.ts', ['--root', root], root);
|
|
132
|
+
try {
|
|
133
|
+
return runSelfTest({
|
|
134
|
+
gate: 'check_skill_activation_claim',
|
|
135
|
+
minCases: 4,
|
|
136
|
+
minRejectCases: 3,
|
|
137
|
+
cases: [
|
|
138
|
+
{
|
|
139
|
+
name: 'a turn count that moved away from the record is rejected',
|
|
140
|
+
expect: 'reject',
|
|
141
|
+
run: () => run(write('turns', agreeing.replace('1,234 assistant', '9,999 assistant'))),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'a population redrawn to enlarge the human-named remainder is rejected',
|
|
145
|
+
expect: 'reject',
|
|
146
|
+
run: () => run(write('population', agreeing.replace('remaining 195 are', 'remaining 290 are'))),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'a claim that stops stating the population split is rejected',
|
|
150
|
+
expect: 'reject',
|
|
151
|
+
run: () => run(write('dropped', agreeing.slice(0, agreeing.indexOf('. 10 declare') + 1))),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'a claim reproducing every recorded figure passes',
|
|
155
|
+
expect: 'accept',
|
|
156
|
+
run: () => run(write('agreeing', agreeing)),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function main(argv: readonly string[]): number {
|
|
166
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
167
|
+
process.stdout.write('usage: check_skill_activation_claim [--root DIR] [--self-test]\n');
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (argv.includes('--self-test')) {
|
|
171
|
+
return selfTest();
|
|
172
|
+
}
|
|
173
|
+
const i = argv.indexOf('--root');
|
|
174
|
+
return check(i === -1 ? REPO_ROOT : (argv[i + 1] ?? REPO_ROOT));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
178
|
+
process.exit(main(process.argv.slice(2)));
|
|
179
|
+
}
|
|
@@ -150,7 +150,7 @@ function _check_one(p: string, today: PyDate): string[] {
|
|
|
150
150
|
if (age > MAX_AGE_DAYS) {
|
|
151
151
|
errors.push(
|
|
152
152
|
`${rel}: \`last_eval\` ${raw as string} is ${age}d old (> ${MAX_AGE_DAYS}d) ` +
|
|
153
|
-
'— re-run skill_trigger_eval
|
|
153
|
+
'— re-run skill_trigger_eval and bump it',
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -53,8 +53,13 @@ import { entryText, isSidechain } from "./_lib/transcript_entry.js";
|
|
|
53
53
|
|
|
54
54
|
import { classify } from "./language_mirror_hook.js";
|
|
55
55
|
import { isSyntheticPrompt } from "./_lib/prompt_shape.js";
|
|
56
|
-
import {
|
|
57
|
-
|
|
56
|
+
import {
|
|
57
|
+
classifyAuthorization,
|
|
58
|
+
extractObjects,
|
|
59
|
+
OBJECT_REQUIRED_OPS,
|
|
60
|
+
type GitOp,
|
|
61
|
+
} from "./git_authorization_hook.js";
|
|
62
|
+
import { BLOCK_OPS, commandObject, commandOp } from "./hooks/git_command_classifier.js";
|
|
58
63
|
import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.js";
|
|
59
64
|
// Round 7 § 1.5 — the SAME predicate `turn-end-gate` refuses on, imported rather
|
|
60
65
|
// than reimplemented, so the measured rate and the gate cannot disagree.
|
|
@@ -579,6 +584,8 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
579
584
|
let pinned: "de" | "en" | null = null;
|
|
580
585
|
let pendingPoll: { at: string; cmd: string } | null = null;
|
|
581
586
|
let authorized = new Set<GitOp>();
|
|
587
|
+
// Per-op objects the current turn's prompt named. Reset with `authorized`.
|
|
588
|
+
let authorizedObjects = new Map<GitOp, Set<string>>();
|
|
582
589
|
let sawPending = false;
|
|
583
590
|
let evaluationsThisTurn = 0;
|
|
584
591
|
// Round-5 provenance for language violations. The round-5 audit could only
|
|
@@ -723,7 +730,15 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
723
730
|
if (c.language !== "und") {
|
|
724
731
|
pinned = c.language;
|
|
725
732
|
}
|
|
726
|
-
|
|
733
|
+
const classified = classifyAuthorization(ut);
|
|
734
|
+
authorized = new Set(classified.authorized);
|
|
735
|
+
// Step 2.2 — the objects the turn NAMED, per operation. Rebuilt on every
|
|
736
|
+
// counted user turn for the same reason `authorized` is: an authorization
|
|
737
|
+
// for one table does not carry into the next prompt any more than the
|
|
738
|
+
// operation does.
|
|
739
|
+
authorizedObjects = new Map(
|
|
740
|
+
classified.authorized.map((op) => [op, new Set(extractObjects(op, ut))]),
|
|
741
|
+
);
|
|
727
742
|
sawPending = false;
|
|
728
743
|
evaluationsThisTurn = 0;
|
|
729
744
|
pendingPoll = null;
|
|
@@ -811,6 +826,32 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
811
826
|
at,
|
|
812
827
|
detail: `irreversible \`${op}\` with no authorization in the turn's prompt: ${cmd.slice(0, 110)}`,
|
|
813
828
|
});
|
|
829
|
+
} else if (op !== null && authorized.has(op) && OBJECT_REQUIRED_OPS.has(op)) {
|
|
830
|
+
// Step 2.2 — the operation WAS authorized and ran against something
|
|
831
|
+
// the turn never named. An authorization for one table is not an
|
|
832
|
+
// authorization for another, and until the ledger could carry an
|
|
833
|
+
// object this was indistinguishable from a clean turn.
|
|
834
|
+
//
|
|
835
|
+
// This REPORTS and refuses nothing — ADR-254 removed the enforcing
|
|
836
|
+
// reader, and the shape it preserved is a count after the fact.
|
|
837
|
+
//
|
|
838
|
+
// A command naming NO object produces no finding: `commandObject`
|
|
839
|
+
// returns null there, and a guessed object would manufacture a
|
|
840
|
+
// violation out of a parse failure. Under-reporting is the safe
|
|
841
|
+
// direction for a measurement.
|
|
842
|
+
const object = commandObject(op, cmd);
|
|
843
|
+
const named = authorizedObjects.get(op);
|
|
844
|
+
if (object !== null && !(named?.has(object) ?? false)) {
|
|
845
|
+
report.violations.push({
|
|
846
|
+
check: "git-authorization",
|
|
847
|
+
session: sessionId,
|
|
848
|
+
at,
|
|
849
|
+
detail:
|
|
850
|
+
`\`${op}\` authorized, but its object \`${object}\` is not one the ` +
|
|
851
|
+
`turn's prompt named (${named && named.size > 0 ? [...named].join(", ") : "no object named"}): ` +
|
|
852
|
+
cmd.slice(0, 110),
|
|
853
|
+
});
|
|
854
|
+
}
|
|
814
855
|
}
|
|
815
856
|
if (isCiPoll(cmd)) {
|
|
816
857
|
// The RESULT of this call arrives on the NEXT entry, and that entry is
|
|
@@ -690,7 +690,7 @@ function renderHygieneSection(hygiene: HygieneReadResult): string[] {
|
|
|
690
690
|
out.push(`_Source: tried ${hygiene.checkedPaths.map((p) => `\`${p}\``).join(', then ')}_`);
|
|
691
691
|
out.push('');
|
|
692
692
|
if (hygiene.state === null) {
|
|
693
|
-
out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or
|
|
693
|
+
out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or no PostToolUse hook is bound on this host)');
|
|
694
694
|
return out;
|
|
695
695
|
}
|
|
696
696
|
const s = hygiene.state;
|
|
@@ -136,7 +136,7 @@ const _FN_SPEC: Record<string, FnSpec> = {
|
|
|
136
136
|
// Surfaces the INSTALLER provides outside generate_tools() (so the derivation
|
|
137
137
|
// guard stays pure). Marked with a † footnote in the rendered matrix.
|
|
138
138
|
const _INSTALL_TIME_CELLS: Record<string, Record<string, string>> = {
|
|
139
|
-
rules: { copilot: 'adapter' }, // .github/copilot-instructions.md (aggregated,
|
|
139
|
+
rules: { copilot: 'adapter' }, // .github/copilot-instructions.md (aggregated, the installer)
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
// Python: re.compile(r"\b(generate_[A-Za-z0-9_]+)\s*\(").
|
|
@@ -61,6 +61,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
61
61
|
|
|
62
62
|
import { humanTypedThisTurn as _humanTypedThisTurn } from "./_lib/machine_wake.js";
|
|
63
63
|
|
|
64
|
+
import {
|
|
65
|
+
CONSEQUENCE_PHRASES,
|
|
66
|
+
extractObjects,
|
|
67
|
+
} from "./_lib/consequence_objects.js";
|
|
64
68
|
import { atomic_write_json } from "./hooks/state_io.js";
|
|
65
69
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
66
70
|
|
|
@@ -235,7 +239,36 @@ export type GitOp =
|
|
|
235
239
|
/** Deleting a branch, locally or on the remote. */
|
|
236
240
|
| "branch-delete"
|
|
237
241
|
/** Closing a pull request or an issue. */
|
|
238
|
-
| "close"
|
|
242
|
+
| "close"
|
|
243
|
+
|
|
244
|
+
// ── Consequence operations, added 2026-09-06
|
|
245
|
+
// (`road-to-authorization-that-reaches-further` 2.1).
|
|
246
|
+
//
|
|
247
|
+
// The boundaries the owner named had NO vocabulary at all, so an
|
|
248
|
+
// authorization the user gave in plain words was invisible to the record and
|
|
249
|
+
// the matching operation was invisible to the count. These seven are NOT git
|
|
250
|
+
// operations, and the union's name is now narrower than its contents — a
|
|
251
|
+
// rename touches every consumer and buys nothing the doc comments do not.
|
|
252
|
+
//
|
|
253
|
+
// RECORDING AN AUTHORIZATION IS NOT GRANTING ONE. After ADR-254 there is no
|
|
254
|
+
// grantor left in this package: the ledger concern is advisory by manifest
|
|
255
|
+
// and the only consumer that acts on this vocabulary is the after-the-fact
|
|
256
|
+
// count in `conformance_scan`. Nothing here refuses, and nothing here may be
|
|
257
|
+
// read as a step toward refusing.
|
|
258
|
+
/** Deploying to a production environment. */
|
|
259
|
+
| "prod-deploy"
|
|
260
|
+
/** Destroying production data — a table, a bucket, a collection, rows. */
|
|
261
|
+
| "prod-data-destroy"
|
|
262
|
+
/** Changing production infrastructure — cluster, IAM, DNS, network. */
|
|
263
|
+
| "prod-infra-change"
|
|
264
|
+
/** Sending something outward that a third party receives. */
|
|
265
|
+
| "external-send"
|
|
266
|
+
/** Moving money — a charge, a refund, a payout, a transfer. */
|
|
267
|
+
| "money-movement"
|
|
268
|
+
/** Force-pushing a shared trunk, as distinct from a feature branch. */
|
|
269
|
+
| "trunk-force-push"
|
|
270
|
+
/** Destroying work outside the task's own scope. */
|
|
271
|
+
| "out-of-scope-destruction";
|
|
239
272
|
|
|
240
273
|
export const ALL_OPS: readonly GitOp[] = [
|
|
241
274
|
"commit",
|
|
@@ -264,6 +297,13 @@ export const ALL_OPS: readonly GitOp[] = [
|
|
|
264
297
|
"stash-drop",
|
|
265
298
|
"branch-delete",
|
|
266
299
|
"close",
|
|
300
|
+
"prod-deploy",
|
|
301
|
+
"prod-data-destroy",
|
|
302
|
+
"prod-infra-change",
|
|
303
|
+
"external-send",
|
|
304
|
+
"money-movement",
|
|
305
|
+
"trunk-force-push",
|
|
306
|
+
"out-of-scope-destruction",
|
|
267
307
|
];
|
|
268
308
|
|
|
269
309
|
/**
|
|
@@ -432,6 +472,8 @@ const PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
432
472
|
op: "pr-merge-auto",
|
|
433
473
|
re: /\bauto[-\s]?merge\b|\bmerge\b[^.\n]{0,20}\bautomatisch\b|\bautomatisch\b[^.\n]{0,20}\bmerg(e|en)\b/i,
|
|
434
474
|
},
|
|
475
|
+
|
|
476
|
+
...CONSEQUENCE_PHRASES,
|
|
435
477
|
];
|
|
436
478
|
|
|
437
479
|
/**
|
|
@@ -615,6 +657,25 @@ export interface Ledger extends JsonObject {
|
|
|
615
657
|
* as "no grants", which is the pre-ADR-252 behaviour exactly.
|
|
616
658
|
*/
|
|
617
659
|
grants?: MergeGrant[];
|
|
660
|
+
/**
|
|
661
|
+
* The objects a consequence authorization named, per operation.
|
|
662
|
+
*
|
|
663
|
+
* An authorization for one table, one environment or one recipient class is
|
|
664
|
+
* not an authorization for another, and until now the record could not
|
|
665
|
+
* express the difference for anything but pull-request numbers
|
|
666
|
+
* (`MergeGrant.targets`). This is that distinction generalised to the
|
|
667
|
+
* consequence vocabulary.
|
|
668
|
+
*
|
|
669
|
+
* IT IS A RECORDED FIELD, NOT A MATCHING CONDITION. Nothing consults it to
|
|
670
|
+
* refuse anything; the one consumer is `conformance_scan`, which counts
|
|
671
|
+
* after the fact when an executed operation's object is absent here. Making
|
|
672
|
+
* it a matching condition is the ledger item ADR-254 left to the owner, and
|
|
673
|
+
* it is deliberately not taken here.
|
|
674
|
+
*
|
|
675
|
+
* Optional, so a ledger written by an older build still parses. Absent reads
|
|
676
|
+
* as "no objects named", which is exactly the pre-2.2 behaviour.
|
|
677
|
+
*/
|
|
678
|
+
objects?: { [op: string]: string[] };
|
|
618
679
|
}
|
|
619
680
|
|
|
620
681
|
/**
|
|
@@ -1281,6 +1342,7 @@ export function takePending(
|
|
|
1281
1342
|
* file for the discriminator and the captured evidence behind it.
|
|
1282
1343
|
*/
|
|
1283
1344
|
export { humanTypedThisTurn } from "./_lib/machine_wake.js";
|
|
1345
|
+
export { extractObjects, OBJECT_REQUIRED_OPS } from "./_lib/consequence_objects.js";
|
|
1284
1346
|
|
|
1285
1347
|
export function run(stdin_text: string, options: { consumer_root: string }): number {
|
|
1286
1348
|
let envelope: JsonObject = {};
|
|
@@ -1332,6 +1394,17 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
|
|
|
1332
1394
|
`"${prompt.trim().slice(0, 40)}"`;
|
|
1333
1395
|
}
|
|
1334
1396
|
|
|
1397
|
+
// The objects the prose named, per authorized operation. Recorded for every
|
|
1398
|
+
// consequence op that names one; `OBJECT_REQUIRED_OPS` is the subset whose
|
|
1399
|
+
// ABSENCE the conformance count reads as a finding, and that reading lives in
|
|
1400
|
+
// the scan rather than here — this side records what was said, and never
|
|
1401
|
+
// decides what it permits.
|
|
1402
|
+
const objects: { [op: string]: string[] } = {};
|
|
1403
|
+
for (const op of authorized) {
|
|
1404
|
+
const named = extractObjects(op, prompt);
|
|
1405
|
+
if (named.length > 0) objects[op] = named;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1335
1408
|
const now = new Date();
|
|
1336
1409
|
const ledger: Ledger = {
|
|
1337
1410
|
session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
|
|
@@ -1339,6 +1412,7 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
|
|
|
1339
1412
|
authorized,
|
|
1340
1413
|
evidence,
|
|
1341
1414
|
prompt_chars: prompt.length,
|
|
1415
|
+
objects,
|
|
1342
1416
|
// Grants survive this write; `authorized` does not. That asymmetry IS the
|
|
1343
1417
|
// decision. A bare operation name stays one-shot exactly as `commit-policy`
|
|
1344
1418
|
// requires, and only an authorization that also froze its objects earns a
|