@event4u/agent-config 11.0.0 → 12.0.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +71 -249
- package/dist/agent-src/commands/agent-handoff.md +0 -1
- package/dist/agent-src/commands/agent-status.md +0 -1
- package/dist/agent-src/commands/agents/audit.md +0 -1
- package/dist/agent-src/commands/agents/init.md +0 -1
- package/dist/agent-src/commands/agents/optimize.md +0 -1
- package/dist/agent-src/commands/agents/user/accept.md +0 -1
- package/dist/agent-src/commands/agents/user/delete.md +0 -1
- package/dist/agent-src/commands/agents/user/init.md +0 -1
- package/dist/agent-src/commands/agents/user/review.md +0 -1
- package/dist/agent-src/commands/agents/user/show.md +0 -1
- package/dist/agent-src/commands/agents/user/update.md +0 -1
- package/dist/agent-src/commands/agents/user.md +0 -1
- package/dist/agent-src/commands/agents.md +0 -1
- package/dist/agent-src/commands/analytics/prune.md +0 -1
- package/dist/agent-src/commands/analytics/show.md +0 -1
- package/dist/agent-src/commands/analytics.md +0 -1
- package/dist/agent-src/commands/analyze/conformance.md +0 -1
- package/dist/agent-src/commands/analyze/decision.md +0 -1
- package/dist/agent-src/commands/analyze/inbox.md +0 -1
- package/dist/agent-src/commands/analyze/incident.md +0 -1
- package/dist/agent-src/commands/analyze/near-miss.md +0 -1
- package/dist/agent-src/commands/analyze/postmortem.md +0 -1
- package/dist/agent-src/commands/analyze/premortem.md +0 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +0 -1
- package/dist/agent-src/commands/analyze.md +0 -1
- package/dist/agent-src/commands/brand/identity.md +0 -1
- package/dist/agent-src/commands/brand/review.md +0 -1
- package/dist/agent-src/commands/brand/strategy.md +0 -1
- package/dist/agent-src/commands/brand/tokens.md +0 -1
- package/dist/agent-src/commands/brand/voice.md +0 -1
- package/dist/agent-src/commands/brand.md +0 -1
- package/dist/agent-src/commands/bug/fix.md +0 -1
- package/dist/agent-src/commands/bug/investigate.md +0 -1
- package/dist/agent-src/commands/bug.md +0 -1
- package/dist/agent-src/commands/challenge-me/vision.md +0 -1
- package/dist/agent-src/commands/challenge-me/with-docs.md +0 -1
- package/dist/agent-src/commands/challenge-me.md +0 -1
- package/dist/agent-src/commands/chat-history/import.md +0 -1
- package/dist/agent-src/commands/chat-history.md +0 -1
- package/dist/agent-src/commands/check-current-md.md +0 -1
- package/dist/agent-src/commands/commit/in-chunks.md +0 -1
- package/dist/agent-src/commands/commit.md +0 -1
- package/dist/agent-src/commands/condense.md +0 -1
- package/dist/agent-src/commands/context/create.md +0 -1
- package/dist/agent-src/commands/context/refactor.md +0 -1
- package/dist/agent-src/commands/context.md +0 -1
- package/dist/agent-src/commands/contribution-precheck.md +0 -1
- package/dist/agent-src/commands/cost/profile.md +0 -1
- package/dist/agent-src/commands/cost/report.md +0 -1
- package/dist/agent-src/commands/cost.md +0 -1
- package/dist/agent-src/commands/council/analysis.md +0 -1
- package/dist/agent-src/commands/council/debate.md +0 -1
- package/dist/agent-src/commands/council/default.md +0 -1
- package/dist/agent-src/commands/council/design.md +0 -1
- package/dist/agent-src/commands/council/optimize.md +0 -1
- package/dist/agent-src/commands/council/pr.md +0 -1
- package/dist/agent-src/commands/council.md +0 -1
- package/dist/agent-src/commands/design-system/capture.md +0 -1
- package/dist/agent-src/commands/design-system/generate.md +0 -1
- package/dist/agent-src/commands/design-system/import.md +0 -1
- package/dist/agent-src/commands/design-system.md +0 -1
- package/dist/agent-src/commands/estimate-ticket.md +0 -1
- package/dist/agent-src/commands/explain-run.md +0 -1
- package/dist/agent-src/commands/feature/dev.md +0 -1
- package/dist/agent-src/commands/feature/explore.md +0 -1
- package/dist/agent-src/commands/feature/plan.md +0 -1
- package/dist/agent-src/commands/feature/refactor.md +0 -1
- package/dist/agent-src/commands/feature/roadmap.md +0 -1
- package/dist/agent-src/commands/feature.md +0 -1
- package/dist/agent-src/commands/fix/ci.md +0 -1
- package/dist/agent-src/commands/fix/comments.md +0 -1
- package/dist/agent-src/commands/fix/portability.md +0 -1
- package/dist/agent-src/commands/fix/pr-comments-loop.md +0 -1
- package/dist/agent-src/commands/fix/pr-comments.md +0 -1
- package/dist/agent-src/commands/fix/quality.md +0 -1
- package/dist/agent-src/commands/fix/refs.md +0 -1
- package/dist/agent-src/commands/fix/route.md +0 -1
- package/dist/agent-src/commands/fix/seeder.md +0 -1
- package/dist/agent-src/commands/fix.md +0 -1
- package/dist/agent-src/commands/ghostwriter/delete.md +0 -1
- package/dist/agent-src/commands/ghostwriter/fetch.md +0 -1
- package/dist/agent-src/commands/ghostwriter/list.md +0 -1
- package/dist/agent-src/commands/ghostwriter/show.md +0 -1
- package/dist/agent-src/commands/ghostwriter/write.md +0 -1
- package/dist/agent-src/commands/ghostwriter.md +0 -1
- package/dist/agent-src/commands/grill-me.md +0 -1
- package/dist/agent-src/commands/humanize.md +0 -1
- package/dist/agent-src/commands/image/analyse.md +0 -1
- package/dist/agent-src/commands/image/create.md +0 -1
- package/dist/agent-src/commands/image/verify.md +0 -1
- package/dist/agent-src/commands/image.md +0 -1
- package/dist/agent-src/commands/implement-ticket.md +0 -1
- package/dist/agent-src/commands/jira-ticket.md +0 -1
- package/dist/agent-src/commands/judge/on-diff.md +0 -1
- package/dist/agent-src/commands/judge/solo.md +0 -1
- package/dist/agent-src/commands/judge/steps.md +0 -1
- package/dist/agent-src/commands/judge.md +0 -1
- package/dist/agent-src/commands/knowledge/cross-repo.md +0 -1
- package/dist/agent-src/commands/knowledge/forget.md +0 -1
- package/dist/agent-src/commands/knowledge/ingest.md +0 -1
- package/dist/agent-src/commands/knowledge/list.md +0 -1
- package/dist/agent-src/commands/knowledge.md +0 -1
- package/dist/agent-src/commands/memory/add.md +0 -1
- package/dist/agent-src/commands/memory/learn-low-impact.md +0 -1
- package/dist/agent-src/commands/memory/load.md +0 -1
- package/dist/agent-src/commands/memory/mine-session.md +0 -1
- package/dist/agent-src/commands/memory/promote.md +0 -1
- package/dist/agent-src/commands/memory/propose.md +0 -1
- package/dist/agent-src/commands/memory.md +0 -1
- package/dist/agent-src/commands/mission/upgrade.md +0 -1
- package/dist/agent-src/commands/mode.md +0 -1
- package/dist/agent-src/commands/module/create.md +0 -1
- package/dist/agent-src/commands/module/explore.md +0 -1
- package/dist/agent-src/commands/module.md +0 -1
- package/dist/agent-src/commands/optimize/agents-dir.md +0 -1
- package/dist/agent-src/commands/optimize/augmentignore.md +0 -1
- package/dist/agent-src/commands/optimize/deep.md +0 -1
- package/dist/agent-src/commands/optimize/project.md +0 -1
- package/dist/agent-src/commands/optimize/prompt.md +0 -1
- package/dist/agent-src/commands/optimize/rtk.md +0 -1
- package/dist/agent-src/commands/optimize/skills.md +0 -1
- package/dist/agent-src/commands/optimize.md +0 -1
- package/dist/agent-src/commands/orchestrate.md +0 -1
- package/dist/agent-src/commands/override/create.md +0 -1
- package/dist/agent-src/commands/override/manage.md +0 -1
- package/dist/agent-src/commands/override.md +0 -1
- package/dist/agent-src/commands/package/reset.md +0 -1
- package/dist/agent-src/commands/package/test.md +0 -1
- package/dist/agent-src/commands/package.md +0 -1
- package/dist/agent-src/commands/post-as/ghostwriter.md +0 -1
- package/dist/agent-src/commands/post-as/me.md +0 -1
- package/dist/agent-src/commands/post-as.md +0 -1
- package/dist/agent-src/commands/pr/create/description-only.md +0 -1
- package/dist/agent-src/commands/pr/create.md +23 -9
- package/dist/agent-src/commands/prediction-pool.md +0 -1
- package/dist/agent-src/commands/prepare-for-review.md +0 -1
- package/dist/agent-src/commands/profile/activate.md +0 -1
- package/dist/agent-src/commands/profile/deactivate.md +0 -1
- package/dist/agent-src/commands/profile/show.md +0 -1
- package/dist/agent-src/commands/profile.md +0 -1
- package/dist/agent-src/commands/project/analyze.md +0 -1
- package/dist/agent-src/commands/project/health.md +0 -1
- package/dist/agent-src/commands/project.md +0 -1
- package/dist/agent-src/commands/refine-ticket.md +0 -1
- package/dist/agent-src/commands/research/deep.md +0 -1
- package/dist/agent-src/commands/research/report.md +0 -1
- package/dist/agent-src/commands/research.md +0 -1
- package/dist/agent-src/commands/review/changes.md +0 -1
- package/dist/agent-src/commands/review/routing.md +0 -1
- package/dist/agent-src/commands/review.md +0 -1
- package/dist/agent-src/commands/roadmap/ai-council.md +0 -1
- package/dist/agent-src/commands/roadmap/create.md +0 -1
- package/dist/agent-src/commands/roadmap/materialize.md +0 -1
- package/dist/agent-src/commands/roadmap/next.md +0 -1
- package/dist/agent-src/commands/roadmap/process-full.md +0 -1
- package/dist/agent-src/commands/roadmap/process-phase.md +0 -1
- package/dist/agent-src/commands/roadmap/process-step.md +0 -1
- package/dist/agent-src/commands/roadmap.md +0 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +0 -1
- package/dist/agent-src/commands/security-audit-config.md +0 -1
- package/dist/agent-src/commands/skill/preview.md +0 -1
- package/dist/agent-src/commands/skill.md +0 -1
- package/dist/agent-src/commands/skills/discover.md +0 -1
- package/dist/agent-src/commands/skills.md +0 -1
- package/dist/agent-src/commands/sync/agent-settings.md +0 -1
- package/dist/agent-src/commands/sync/gitignore/fix.md +0 -1
- package/dist/agent-src/commands/sync/gitignore.md +0 -1
- package/dist/agent-src/commands/sync.md +0 -1
- package/dist/agent-src/commands/tdd/green.md +0 -1
- package/dist/agent-src/commands/tdd/red.md +0 -1
- package/dist/agent-src/commands/tdd/refactor.md +0 -1
- package/dist/agent-src/commands/tdd.md +0 -1
- package/dist/agent-src/commands/team/adversarial.md +0 -1
- package/dist/agent-src/commands/team/delegate.md +0 -1
- package/dist/agent-src/commands/team/review.md +0 -1
- package/dist/agent-src/commands/team/status.md +0 -1
- package/dist/agent-src/commands/team-knowledge/bootstrap.md +0 -1
- package/dist/agent-src/commands/team-knowledge/consolidate.md +0 -1
- package/dist/agent-src/commands/team-knowledge.md +0 -1
- package/dist/agent-src/commands/team.md +0 -1
- package/dist/agent-src/commands/tests/create.md +0 -1
- package/dist/agent-src/commands/tests/e2e-heal.md +0 -1
- package/dist/agent-src/commands/tests/e2e-plan.md +0 -1
- package/dist/agent-src/commands/tests/execute.md +0 -1
- package/dist/agent-src/commands/tests.md +0 -1
- package/dist/agent-src/commands/threat-model.md +0 -1
- package/dist/agent-src/commands/update-form-request-messages.md +0 -1
- package/dist/agent-src/commands/upstream-contribute.md +0 -1
- package/dist/agent-src/commands/video/from-script.md +0 -1
- package/dist/agent-src/commands/video/from-song.md +0 -1
- package/dist/agent-src/commands/video/scene.md +0 -1
- package/dist/agent-src/commands/video/stitch.md +0 -1
- package/dist/agent-src/commands/video/storyboard.md +0 -1
- package/dist/agent-src/commands/video.md +0 -1
- package/dist/agent-src/commands/work.md +0 -1
- package/dist/agent-src/commands/worktree/cleanup.md +0 -1
- package/dist/agent-src/commands/worktree/create.md +0 -1
- package/dist/agent-src/commands/worktree/status.md +0 -1
- package/dist/agent-src/commands/worktree/verify.md +0 -1
- package/dist/agent-src/commands/worktree.md +0 -1
- package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +9 -1
- package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/commands.candidates.test.js +1 -1
- package/dist/cli/commands/commands.candidates.test.js.map +1 -1
- package/dist/cli/commands/commands.js +9 -15
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/commands/profiles.test.js +7 -7
- package/dist/cli/commands/profiles.test.js.map +1 -1
- package/dist/cli/discovery/loadManifest.js.map +1 -1
- package/dist/cli/main.js +2 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli-delegate/cmd_conformance.js +14 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +211 -410
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +14 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +46 -5
- package/docs/MIGRATION.md +1 -0
- package/docs/contracts/settings-classes.md +41 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +34 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_dispatch.bash +42 -5
- package/src/scripts/audit_command_surface.ts +21 -15
- package/src/scripts/build_catalog_index.ts +1 -1
- package/src/scripts/build_discovery_manifest.ts +30 -34
- package/src/scripts/check_branch_freshness.ts +210 -61
- package/src/scripts/gen_discovery_baseline.ts +3 -3
- package/src/scripts/hook_manifest.yaml +20 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
- package/src/scripts/lint_command_tiers.ts +29 -75
- package/src/scripts/lint_originality.ts +101 -14
- package/src/scripts/schemas/command.schema.json +4 -9
- package/src/scripts/schemas/skill.schema.json +10 -1
- package/src/scripts/skill_linter.ts +82 -2
- package/src/scripts/worktree_cleanup_check.ts +111 -7
|
@@ -657,26 +657,13 @@ function _build(strict: boolean): [JsonObject, JsonObject[]] {
|
|
|
657
657
|
// 6.0.0-C: surface command routing metadata. Does not affect the
|
|
658
658
|
// per-file checksum (computed over frontmatter, below).
|
|
659
659
|
if (category === 'command' && fm !== null && _isObject(fm)) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
//
|
|
664
|
-
// `
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
// top-level `deprecations` block (ADR-092 / road-to-tier-removal)
|
|
668
|
-
// announces the `tier` deprecation; removal is Phase 4. Prefer the
|
|
669
|
-
// explicit field; derive from tier when absent.
|
|
670
|
-
const _tierToVis: Record<number, string> = { 0: 'visible', 1: 'advanced', 2: 'internal' };
|
|
671
|
-
let _vis: Json = fm['visibility'];
|
|
672
|
-
if (
|
|
673
|
-
(_vis === undefined || _vis === null) &&
|
|
674
|
-
fm['tier'] !== undefined &&
|
|
675
|
-
fm['tier'] !== null
|
|
676
|
-
) {
|
|
677
|
-
// `.get(fm["tier"])` returns None when tier is not 0/1/2 → undefined here.
|
|
678
|
-
_vis = typeof fm['tier'] === 'number' ? _tierToVis[fm['tier']] : undefined;
|
|
679
|
-
}
|
|
660
|
+
// ADR-090 named `visibility:` the source of truth; the integer
|
|
661
|
+
// `tier:` alias it superseded was dual-emitted here through the
|
|
662
|
+
// deprecation window and REMOVED in road-to-tier-removal Phase 4.
|
|
663
|
+
// The manifest no longer carries `tier` in any entry — the
|
|
664
|
+
// top-level `deprecations` block records the removal for consumers
|
|
665
|
+
// that still read the old key.
|
|
666
|
+
const _vis: Json = fm['visibility'];
|
|
680
667
|
if (_vis !== undefined && _vis !== null) {
|
|
681
668
|
entry['visibility'] = _vis;
|
|
682
669
|
}
|
|
@@ -788,27 +775,36 @@ function _build(strict: boolean): [JsonObject, JsonObject[]] {
|
|
|
788
775
|
const stats = _compute_stats(artefacts, unassigned, documentedUnassigned);
|
|
789
776
|
|
|
790
777
|
const manifest: JsonObject = {
|
|
791
|
-
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
778
|
+
// v3 (road-to-tier-removal Phase 4): the integer command `tier` is no
|
|
779
|
+
// longer emitted on `category: command` artefacts. That is a breaking
|
|
780
|
+
// change to a published data contract, and this schema's own rule is
|
|
781
|
+
// "bump on breaking change" — so the version carries the break rather
|
|
782
|
+
// than leaving a v2 consumer to discover a silently-absent field.
|
|
783
|
+
version: 3,
|
|
784
|
+
// Machine-readable removal record (road-to-tier-removal Phase 1 added
|
|
785
|
+
// this block as a deprecation ANNOUNCEMENT; Phase 4 turned it into the
|
|
786
|
+
// record of an executed removal). The entry is retained on purpose: a
|
|
787
|
+
// consumer that still reads the old key needs to find out what replaced
|
|
788
|
+
// it and when it went, and an absent entry would say neither. `sunset`
|
|
789
|
+
// is maintainer-owned: SET on 2026-08-13, with the soak wait explicitly
|
|
790
|
+
// waived ("wir haben lange genug gewartet"). ADR-137 had already reduced
|
|
791
|
+
// the re-open trigger set to this one act — a concrete sunset — after
|
|
792
|
+
// withdrawing the second trigger as structurally impossible in a
|
|
793
|
+
// no-runtime package.
|
|
800
794
|
deprecations: [
|
|
801
795
|
{
|
|
802
796
|
key: 'tier',
|
|
803
797
|
scope: 'command',
|
|
804
798
|
replacement: 'visibility',
|
|
805
799
|
since: 'ADR-092',
|
|
806
|
-
sunset:
|
|
800
|
+
sunset: '2026-08-13',
|
|
801
|
+
removed_in: 3,
|
|
807
802
|
note:
|
|
808
|
-
'Integer command `tier` (0/1/2)
|
|
809
|
-
'
|
|
810
|
-
'
|
|
811
|
-
'
|
|
803
|
+
'Integer command `tier` (0/1/2) was a back-compat alias for ' +
|
|
804
|
+
'the named `visibility` field (ADR-090). REMOVED in manifest ' +
|
|
805
|
+
'v3 — command artefacts no longer carry `tier`. Read ' +
|
|
806
|
+
'`visibility` (`visible` / `advanced` / `internal`); the old ' +
|
|
807
|
+
'mapping was 0↔visible, 1↔advanced, 2↔internal.',
|
|
812
808
|
},
|
|
813
809
|
],
|
|
814
810
|
generated_at: _nowUtc(),
|
|
@@ -43,9 +43,16 @@
|
|
|
43
43
|
*
|
|
44
44
|
* So the base is resolved, in order: an explicit `--base`, then the base of the
|
|
45
45
|
* OPEN PR for this branch as the forge reports it, then the repo's default
|
|
46
|
-
* branch
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* branch AS THE SERVER REPORTS IT (`ls-remote --symref`), then the local
|
|
47
|
+
* `origin/HEAD`, then `main`. Every message names the base AND where it came
|
|
48
|
+
* from, because "I checked main because that is the PR's base" and "I checked
|
|
49
|
+
* main because I could not ask" must never print the same.
|
|
50
|
+
*
|
|
51
|
+
* The server symref matters for the same reason `remoteHead` exists: the local
|
|
52
|
+
* `refs/remotes/origin/HEAD` is written at clone time and never refreshed, so
|
|
53
|
+
* taking the base NAME from it would reintroduce the stale-local-state failure
|
|
54
|
+
* this module argues against everywhere else. It is kept only as the labelled
|
|
55
|
+
* fallback for when the server cannot be reached.
|
|
49
56
|
*
|
|
50
57
|
* When the forge cannot be asked (no `gh`, not authenticated, no GitHub remote)
|
|
51
58
|
* the run says so out loud even under `--quiet`: it has verified the default
|
|
@@ -66,12 +73,25 @@ const DEFAULT_BASE = "main";
|
|
|
66
73
|
const SELF_TEST_MIN_CASES = 5;
|
|
67
74
|
const SELF_TEST_MIN_REJECT = 2;
|
|
68
75
|
const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
|
|
69
|
-
/**
|
|
70
|
-
|
|
76
|
+
/**
|
|
77
|
+
* One network round trip on the push path — long enough for a cold `gh`, short
|
|
78
|
+
* enough that the worst case stays under budget.
|
|
79
|
+
*
|
|
80
|
+
* It bounds `git` as well as `gh`, and that is the point rather than tidiness:
|
|
81
|
+
* with a timeout on `gh` only, the two `ls-remote` calls could hang unbounded
|
|
82
|
+
* against an unresponsive SSH remote, so the stated worst case was not one. The
|
|
83
|
+
* slowest path makes three calls (forge, symref, base head), so the arithmetic
|
|
84
|
+
* that keeps this under the 25s `pre_push_budget_seconds` ceiling is 3 x 8s.
|
|
85
|
+
*/
|
|
86
|
+
const NETWORK_TIMEOUT_MS = 8_000;
|
|
71
87
|
|
|
72
88
|
function git(args: string[]): string | null {
|
|
73
89
|
try {
|
|
74
|
-
return execFileSync("git", args, {
|
|
90
|
+
return execFileSync("git", args, {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
93
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
94
|
+
}).trim();
|
|
75
95
|
} catch {
|
|
76
96
|
return null;
|
|
77
97
|
}
|
|
@@ -89,12 +109,17 @@ export type GhRunner = (args: string[]) => GhResult;
|
|
|
89
109
|
* green it did not earn.
|
|
90
110
|
*/
|
|
91
111
|
export type ForgeAnswer =
|
|
92
|
-
| { kind: "pr"; base: string; number
|
|
112
|
+
| { kind: "pr"; base: string; number?: number }
|
|
93
113
|
| { kind: "none" }
|
|
94
114
|
| { kind: "unavailable"; reason: string };
|
|
95
115
|
|
|
96
116
|
/** Where the base name came from — printed with every verdict. */
|
|
97
|
-
export type BaseSource =
|
|
117
|
+
export type BaseSource =
|
|
118
|
+
| "flag"
|
|
119
|
+
| "pull-request"
|
|
120
|
+
| "origin-symref"
|
|
121
|
+
| "origin-head"
|
|
122
|
+
| "fallback";
|
|
98
123
|
|
|
99
124
|
export interface BaseResolution {
|
|
100
125
|
readonly base: string;
|
|
@@ -109,7 +134,7 @@ function gh(args: string[]): GhResult {
|
|
|
109
134
|
const out = execFileSync("gh", args, {
|
|
110
135
|
encoding: "utf8",
|
|
111
136
|
stdio: ["ignore", "pipe", "ignore"],
|
|
112
|
-
timeout:
|
|
137
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
113
138
|
});
|
|
114
139
|
return { ok: true, stdout: out.toString() };
|
|
115
140
|
} catch (err) {
|
|
@@ -118,7 +143,7 @@ function gh(args: string[]): GhResult {
|
|
|
118
143
|
return { ok: false, reason: "the gh CLI is not installed" };
|
|
119
144
|
}
|
|
120
145
|
if (code === "ETIMEDOUT") {
|
|
121
|
-
return { ok: false, reason: `gh did not answer within ${
|
|
146
|
+
return { ok: false, reason: `gh did not answer within ${NETWORK_TIMEOUT_MS / 1000}s` };
|
|
122
147
|
}
|
|
123
148
|
return { ok: false, reason: "gh could not answer — not authenticated, or no GitHub remote" };
|
|
124
149
|
}
|
|
@@ -153,11 +178,29 @@ export function askForgeForBase(branch: string, run: GhRunner = gh): ForgeAnswer
|
|
|
153
178
|
if (base === "") {
|
|
154
179
|
return { kind: "unavailable", reason: "gh answered without a baseRefName" };
|
|
155
180
|
}
|
|
156
|
-
|
|
181
|
+
// The number is cosmetic — it names the PR in the verdict line — while the
|
|
182
|
+
// base is the answer the gate exists to get. An unusable number therefore
|
|
183
|
+
// does NOT discard the base: doing that made the gate check the repo default
|
|
184
|
+
// instead and warn "could not ask the forge" when it had asked and been
|
|
185
|
+
// answered, trading a correct verdict for a tidier message. `describeBase`
|
|
186
|
+
// omits the number rather than rendering the impossible `#0`.
|
|
187
|
+
const usable =
|
|
188
|
+
typeof row.number === "number" && Number.isInteger(row.number) && row.number > 0;
|
|
189
|
+
return usable ? { kind: "pr", base, number: row.number as number } : { kind: "pr", base };
|
|
157
190
|
}
|
|
158
191
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The default branch as the LOCAL clone believes it — no network.
|
|
194
|
+
*
|
|
195
|
+
* `refs/remotes/origin/HEAD` is written at clone time and never refreshed
|
|
196
|
+
* without `git remote set-head`, so this is exactly the class of stale local
|
|
197
|
+
* state this module argues against everywhere else. It has exactly one caller,
|
|
198
|
+
* `serverDefaultBase`, and is reached only when the server could not be asked —
|
|
199
|
+
* where `describeBase` labels it as possibly stale. It decides no verdict on
|
|
200
|
+
* its own; a short-circuit that let it do so is the regression documented in
|
|
201
|
+
* `resolveBase`.
|
|
202
|
+
*/
|
|
203
|
+
export function localDefaultBase(): { base: string; source: "origin-head" | "fallback" } {
|
|
161
204
|
const ref = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
162
205
|
if (ref !== null && ref.startsWith("origin/")) {
|
|
163
206
|
const name = ref.slice("origin/".length).trim();
|
|
@@ -168,24 +211,90 @@ export function repoDefaultBase(): { base: string; source: "origin-head" | "fall
|
|
|
168
211
|
return { base: DEFAULT_BASE, source: "fallback" };
|
|
169
212
|
}
|
|
170
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The default branch as the SERVER reports it right now.
|
|
216
|
+
*
|
|
217
|
+
* Same discipline as `remoteHead`: a gate whose thesis is "ask the remote"
|
|
218
|
+
* must not take the branch NAME from a tracking ref either. Costs one
|
|
219
|
+
* `ls-remote`, the round trip this gate already pays, and degrades to the
|
|
220
|
+
* local ref (labelled as such) when the server cannot be reached.
|
|
221
|
+
*/
|
|
222
|
+
export function serverDefaultBase(): {
|
|
223
|
+
base: string;
|
|
224
|
+
source: "origin-symref" | "origin-head" | "fallback";
|
|
225
|
+
} {
|
|
226
|
+
const out = git(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
227
|
+
if (out !== null) {
|
|
228
|
+
for (const line of out.split("\n")) {
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
if (trimmed.startsWith("ref:")) {
|
|
231
|
+
const name = trimmed.slice("ref:".length).trim().split(/\s+/)[0] ?? "";
|
|
232
|
+
if (name.startsWith("refs/heads/")) {
|
|
233
|
+
const short = name.slice("refs/heads/".length).trim();
|
|
234
|
+
if (short !== "") {
|
|
235
|
+
return { base: short, source: "origin-symref" };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return localDefaultBase();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The base requested on the command line, or null when none was.
|
|
246
|
+
*
|
|
247
|
+
* Both spellings are accepted. `--base=develop` used to be dropped silently by
|
|
248
|
+
* an `indexOf("--base")` lookup, and a dropped request is worse than a rejected
|
|
249
|
+
* one here: the gate then auto-resolves some OTHER base and reports a green
|
|
250
|
+
* about a base the caller never asked about.
|
|
251
|
+
*/
|
|
252
|
+
export function explicitBase(argv: readonly string[]): string | null {
|
|
253
|
+
const joined = argv.find((a) => a.startsWith("--base="));
|
|
254
|
+
if (joined !== undefined) {
|
|
255
|
+
const value = joined.slice("--base=".length);
|
|
256
|
+
return value === "" ? null : value;
|
|
257
|
+
}
|
|
258
|
+
const flag = argv.indexOf("--base");
|
|
259
|
+
if (flag < 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const explicit = argv[flag + 1];
|
|
263
|
+
// A bare trailing `--base`, or one followed by another flag, is a typo rather
|
|
264
|
+
// than a base name. Accepting it produced `remoteHead("--quiet")`, which fails
|
|
265
|
+
// and lands on the exit-0 NOT-VERIFIED path — a typo reading as a pass.
|
|
266
|
+
if (explicit === undefined || explicit === "" || explicit.startsWith("-")) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return explicit;
|
|
270
|
+
}
|
|
271
|
+
|
|
171
272
|
/** `--base` wins, then the open PR's base, then the repo default. */
|
|
172
273
|
export function resolveBase(
|
|
173
274
|
argv: readonly string[],
|
|
174
275
|
branch: string,
|
|
175
276
|
forge: (b: string) => ForgeAnswer = (b) => askForgeForBase(b),
|
|
176
277
|
): BaseResolution {
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
if (explicit !== undefined && explicit !== "") {
|
|
181
|
-
return { base: explicit, source: "flag" };
|
|
182
|
-
}
|
|
278
|
+
const explicit = explicitBase(argv);
|
|
279
|
+
if (explicit !== null) {
|
|
280
|
+
return { base: explicit, source: "flag" };
|
|
183
281
|
}
|
|
282
|
+
// There is deliberately no cheap short-circuit for "the branch looks like the
|
|
283
|
+
// default". It was tried and reverted: it decided a VERDICT from
|
|
284
|
+
// `localDefaultBase`, the stale clone-time ref this module argues against, so
|
|
285
|
+
// a branch whose name collided with a stale or fallback local default was
|
|
286
|
+
// skipped without any check at all. The premise it rested on — that
|
|
287
|
+
// `gh pr list --head main` can never usefully answer — is false: a PR from the
|
|
288
|
+
// default branch into a release line or a parent PR is exactly that answer,
|
|
289
|
+
// and it is the case this gate exists for. Standing on the real base is
|
|
290
|
+
// short-circuited one layer up, in `main`, AFTER the base is known.
|
|
184
291
|
const answer = forge(branch);
|
|
185
292
|
if (answer.kind === "pr") {
|
|
186
|
-
return
|
|
293
|
+
return answer.number === undefined
|
|
294
|
+
? { base: answer.base, source: "pull-request" }
|
|
295
|
+
: { base: answer.base, source: "pull-request", pr: answer.number };
|
|
187
296
|
}
|
|
188
|
-
const fallback =
|
|
297
|
+
const fallback = serverDefaultBase();
|
|
189
298
|
return answer.kind === "unavailable"
|
|
190
299
|
? { base: fallback.base, source: fallback.source, unverified: answer.reason }
|
|
191
300
|
: { base: fallback.base, source: fallback.source };
|
|
@@ -197,9 +306,13 @@ export function describeBase(r: BaseResolution): string {
|
|
|
197
306
|
case "flag":
|
|
198
307
|
return "base given on the command line";
|
|
199
308
|
case "pull-request":
|
|
200
|
-
return
|
|
309
|
+
return r.pr === undefined
|
|
310
|
+
? "base of the open PR for this branch"
|
|
311
|
+
: `base of open PR #${r.pr}`;
|
|
312
|
+
case "origin-symref":
|
|
313
|
+
return "repo default branch, as the server reports it";
|
|
201
314
|
case "origin-head":
|
|
202
|
-
return "repo default branch
|
|
315
|
+
return "repo default branch from the LOCAL origin/HEAD ref, which may be stale";
|
|
203
316
|
case "fallback":
|
|
204
317
|
return `repo default (assumed ${DEFAULT_BASE} — origin/HEAD is not set)`;
|
|
205
318
|
}
|
|
@@ -243,7 +356,15 @@ export function containsCommit(sha: string): boolean | null {
|
|
|
243
356
|
* gate reading the tracking ref would pass it — which is exactly the false green
|
|
244
357
|
* this gate exists to remove.
|
|
245
358
|
*/
|
|
246
|
-
|
|
359
|
+
interface Scaffold {
|
|
360
|
+
origin: string;
|
|
361
|
+
work: string;
|
|
362
|
+
g: (args: string[], at: string) => void;
|
|
363
|
+
write: (at: string, name: string, body: string) => void;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The bare origin + working clone every fixture below starts from. */
|
|
367
|
+
function scaffold(root: string): Scaffold {
|
|
247
368
|
const origin = path.join(root, "origin.git");
|
|
248
369
|
const work = path.join(root, "work");
|
|
249
370
|
fs.mkdirSync(origin, { recursive: true });
|
|
@@ -260,6 +381,11 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
|
260
381
|
g(["remote", "add", "origin", origin], work);
|
|
261
382
|
write(work, "a.txt", "one");
|
|
262
383
|
g(["push", "-u", "origin", DEFAULT_BASE], work);
|
|
384
|
+
return { origin, work, g, write };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
388
|
+
const { origin, work, g, write } = scaffold(root);
|
|
263
389
|
g(["checkout", "-b", "feat/x"], work);
|
|
264
390
|
write(work, "b.txt", "two");
|
|
265
391
|
if (opts.advanceRemote) {
|
|
@@ -272,37 +398,31 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
|
272
398
|
}
|
|
273
399
|
|
|
274
400
|
/**
|
|
275
|
-
* A repo where the branch's real base is NOT
|
|
276
|
-
*
|
|
277
|
-
*
|
|
401
|
+
* A repo where the branch's real base is NOT `main`: `feat/x` is cut from
|
|
402
|
+
* `develop`, `develop` then advances on the remote, and `main` never moves.
|
|
403
|
+
* The SERVER's `HEAD` is pointed at `serverDefault`.
|
|
278
404
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
405
|
+
* The base is left for the gate to resolve — no `--base`. That is the whole
|
|
406
|
+
* point of the fixture, and the first version got it wrong: it passed
|
|
407
|
+
* `--base` on both cases, which exercises the flag branch that existed before
|
|
408
|
+
* this change. Deleting the entire forge/symref resolution would have left both
|
|
409
|
+
* cases green, so the pair proved nothing about the new behaviour and the raised
|
|
410
|
+
* self-test floors rested on coverage that did not exist. (R2 finding 2.)
|
|
411
|
+
*
|
|
412
|
+
* With `serverDefault = "develop"` the gate must refuse: that is the base this
|
|
413
|
+
* branch is actually behind. With `serverDefault = "main"` the same tree passes.
|
|
414
|
+
* A regression that reinstates a hardcoded `main` flips the first case, because
|
|
415
|
+
* nothing but the resolution decides which base is asked about.
|
|
284
416
|
*/
|
|
285
|
-
function buildTwoBaseFixture(root: string): string {
|
|
286
|
-
const origin =
|
|
287
|
-
const work = path.join(root, "work");
|
|
288
|
-
fs.mkdirSync(origin, { recursive: true });
|
|
289
|
-
const g = (args: string[], at: string): void => {
|
|
290
|
-
execFileSync("git", args, { cwd: at, stdio: "ignore" });
|
|
291
|
-
};
|
|
292
|
-
const write = (at: string, name: string, body: string): void => {
|
|
293
|
-
fs.writeFileSync(path.join(at, name), body);
|
|
294
|
-
g(["add", "-A"], at);
|
|
295
|
-
g(["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", name], at);
|
|
296
|
-
};
|
|
297
|
-
g(["init", "--bare", "-b", DEFAULT_BASE], origin);
|
|
298
|
-
g(["init", "-b", DEFAULT_BASE, work], root);
|
|
299
|
-
g(["remote", "add", "origin", origin], work);
|
|
300
|
-
write(work, "a.txt", "one");
|
|
301
|
-
g(["push", "-u", "origin", DEFAULT_BASE], work);
|
|
417
|
+
function buildTwoBaseFixture(root: string, serverDefault: string): string {
|
|
418
|
+
const { origin, work, g, write } = scaffold(root);
|
|
302
419
|
g(["checkout", "-b", "develop"], work);
|
|
303
420
|
g(["push", "-u", "origin", "develop"], work);
|
|
304
421
|
g(["checkout", "-b", "feat/x"], work);
|
|
305
422
|
write(work, "b.txt", "two");
|
|
423
|
+
// The bare repo's own HEAD is what `ls-remote --symref origin HEAD` reports,
|
|
424
|
+
// so this is the server-side default branch, not a local tracking ref.
|
|
425
|
+
g(["symbolic-ref", "HEAD", `refs/heads/${serverDefault}`], origin);
|
|
306
426
|
// A second clone advances `develop` only — `main` stays exactly where it was.
|
|
307
427
|
const other = path.join(root, "other");
|
|
308
428
|
g(["clone", origin, other], root);
|
|
@@ -312,6 +432,23 @@ function buildTwoBaseFixture(root: string): string {
|
|
|
312
432
|
return work;
|
|
313
433
|
}
|
|
314
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Strip everything that could point the child's `gh` at a real forge.
|
|
437
|
+
*
|
|
438
|
+
* `runGateCli` spawns with the full inherited environment, and since the
|
|
439
|
+
* two-base cases dropped `--base` the resolution itself is what they assert on.
|
|
440
|
+
* An ambient `GH_REPO` can make `gh pr list --head feat/x` answer about a real
|
|
441
|
+
* PR in someone else's repository and silently move the base under test — the
|
|
442
|
+
* same ambient-credential hazard the `forge` seam closed for the unit tests,
|
|
443
|
+
* which would otherwise have stayed live in the only coverage of the new
|
|
444
|
+
* symref resolution. `CI` would no-op every case green.
|
|
445
|
+
*/
|
|
446
|
+
function isolateChildEnv(): void {
|
|
447
|
+
for (const key of ["CI", "GITHUB_ACTIONS", "GH_REPO", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GH_CONFIG_DIR"]) {
|
|
448
|
+
delete process.env[key];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
315
452
|
export function selfTest(repoRoot: string): number {
|
|
316
453
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
|
|
317
454
|
const run = (advanceRemote: boolean, onBase: boolean): number => {
|
|
@@ -320,17 +457,15 @@ export function selfTest(repoRoot: string): number {
|
|
|
320
457
|
if (onBase) {
|
|
321
458
|
execFileSync("git", ["checkout", DEFAULT_BASE], { cwd: work, stdio: "ignore" });
|
|
322
459
|
}
|
|
323
|
-
|
|
324
|
-
delete process.env["CI"];
|
|
325
|
-
delete process.env["GITHUB_ACTIONS"];
|
|
460
|
+
isolateChildEnv();
|
|
326
461
|
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
|
|
327
462
|
};
|
|
328
|
-
const runTwoBase = (
|
|
463
|
+
const runTwoBase = (serverDefault: string): number => {
|
|
329
464
|
const dir = fs.mkdtempSync(path.join(root, "twobase-"));
|
|
330
|
-
const work = buildTwoBaseFixture(dir);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"
|
|
465
|
+
const work = buildTwoBaseFixture(dir, serverDefault);
|
|
466
|
+
isolateChildEnv();
|
|
467
|
+
// No `--base`: the gate resolves it, which is the behaviour under test.
|
|
468
|
+
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
|
|
334
469
|
};
|
|
335
470
|
const cases: SelfTestCase[] = [
|
|
336
471
|
{
|
|
@@ -339,12 +474,12 @@ export function selfTest(repoRoot: string): number {
|
|
|
339
474
|
run: () => run(true, false),
|
|
340
475
|
},
|
|
341
476
|
{
|
|
342
|
-
name: "
|
|
477
|
+
name: "the RESOLVED base decides: server default develop, branch behind it, REFUSED",
|
|
343
478
|
expect: "reject",
|
|
344
479
|
run: () => runTwoBase("develop"),
|
|
345
480
|
},
|
|
346
481
|
{
|
|
347
|
-
name: "
|
|
482
|
+
name: "same tree, server default main — passes, so the resolution is what moved the verdict",
|
|
348
483
|
expect: "accept",
|
|
349
484
|
run: () => runTwoBase(DEFAULT_BASE),
|
|
350
485
|
},
|
|
@@ -389,7 +524,19 @@ function scanReport(scanned: number, allowEmpty?: string): void {
|
|
|
389
524
|
});
|
|
390
525
|
}
|
|
391
526
|
|
|
392
|
-
|
|
527
|
+
/**
|
|
528
|
+
* @param forge Seam for the forge lookup. Production passes nothing and gets the
|
|
529
|
+
* real `gh`. Tests MUST pass a stub: without this parameter the only way to
|
|
530
|
+
* reach the forge-unavailable branch was to not have `gh` installed, so the test
|
|
531
|
+
* for it could only assert conditionally — i.e. it passed whether or not the
|
|
532
|
+
* warning was emitted. It also stopped every fixture test from spawning `gh`
|
|
533
|
+
* against ambient credentials, where a `GH_REPO` override could return a real
|
|
534
|
+
* PR base for a same-named branch and silently change the base under test.
|
|
535
|
+
*/
|
|
536
|
+
export function main(
|
|
537
|
+
argv: string[] = process.argv.slice(2),
|
|
538
|
+
forge?: (b: string) => ForgeAnswer,
|
|
539
|
+
): number {
|
|
393
540
|
if (argv.includes("--self-test")) {
|
|
394
541
|
return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
|
395
542
|
}
|
|
@@ -408,7 +555,9 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
408
555
|
}
|
|
409
556
|
|
|
410
557
|
// Resolved AFTER the branch is known: the PR's base is a fact about THIS branch.
|
|
411
|
-
|
|
558
|
+
// `resolveBase`'s third parameter has a default, so passing `undefined`
|
|
559
|
+
// positionally already selects the real `gh` — no branch needed here.
|
|
560
|
+
const resolved = resolveBase(argv, branch, forge);
|
|
412
561
|
const base = resolved.base;
|
|
413
562
|
|
|
414
563
|
if (branch === base) {
|
|
@@ -247,19 +247,19 @@ export function main(): number {
|
|
|
247
247
|
out.push('');
|
|
248
248
|
|
|
249
249
|
out.push('## Sample commands (first 10)\n');
|
|
250
|
-
out.push('| File | name |
|
|
250
|
+
out.push('| File | name | visibility | cluster |');
|
|
251
251
|
out.push('|---|---|---|---|');
|
|
252
252
|
for (const c of commands.slice(0, 10)) {
|
|
253
253
|
const fm = _frontmatter(c);
|
|
254
254
|
const rel = _relativeTo(c, SRC);
|
|
255
|
-
out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.
|
|
255
|
+
out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.visibility ?? ''} | ${fm.cluster ?? ''} |`);
|
|
256
256
|
}
|
|
257
257
|
out.push('');
|
|
258
258
|
|
|
259
259
|
out.push('## Notes\n');
|
|
260
260
|
out.push('- Every skill carries `domain:` today — Phase 4 mapping uses it as primary signal.');
|
|
261
261
|
out.push('- Rules use `type` (always | auto) + `tier` (safety-floor | kernel | …); no `domain:` today.');
|
|
262
|
-
out.push('- Commands use `
|
|
262
|
+
out.push('- Commands use `visibility` + `cluster`; no `domain:` today.');
|
|
263
263
|
out.push('- Templates have no consistent frontmatter; Phase 4 quarantines them in `unassigned-artefacts.yml`.');
|
|
264
264
|
out.push('');
|
|
265
265
|
|
|
@@ -590,6 +590,26 @@ concerns:
|
|
|
590
590
|
# blocker: in-process Agent-tool subagents inherit the host process env and
|
|
591
591
|
# cannot be marked per-spawn; the axis binds where the suite launches a CLI
|
|
592
592
|
# session itself.
|
|
593
|
+
#
|
|
594
|
+
# Superseded in part, 2026-08-14 (road-to-subagent-lifecycle-integrity Phase 4
|
|
595
|
+
# Step 2). The env half above still stands — nothing marks an in-process spawn
|
|
596
|
+
# per-process. What has changed is that env is no longer the only candidate
|
|
597
|
+
# channel: `subagent_start` / `subagent_stop` are bound on this host
|
|
598
|
+
# (2.1.229, Phase 0 Step 1) and their payload carries `agent_id`, which
|
|
599
|
+
# `subagent-ledger` correlates on today. So a payload-bound role axis —
|
|
600
|
+
# `agent_id` present ⇒ role `worker` — is a live proposal (Phase 4 Step 1),
|
|
601
|
+
# not a closed question.
|
|
602
|
+
# It is NOT implemented, and the reason is a missing measurement rather than a
|
|
603
|
+
# missing decision: whether `agent_id` reaches a `PreToolUse` / `PostToolUse`
|
|
604
|
+
# payload inside a subagent is unverified on this host. That is Phase 0 Step 4,
|
|
605
|
+
# blocked on `raw-capture-needs-host-env` (the capture itself is already
|
|
606
|
+
# shipped — `dispatch_hook._maybe_capture_payload`). A neighbouring assertion
|
|
607
|
+
# from the same spike already came back negative: `agent_type` does NOT arrive
|
|
608
|
+
# on `SubagentStop` (18 of 25 stop records null), so the documented-field
|
|
609
|
+
# presence is not assumed for `agent_id` either.
|
|
610
|
+
# Until that spike lands the env path is the only one wired, and the
|
|
611
|
+
# pre_tool_use exemption above is unaffected either way — a payload-resolved
|
|
612
|
+
# `worker` still cannot drop a pre_tool_use concern.
|
|
593
613
|
roles:
|
|
594
614
|
worker:
|
|
595
615
|
# Orchestrator-only concerns (Phase 2.3): a worker never talks to the
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
* a `subagent_reaped` line is written for it. That line is not bookkeeping —
|
|
74
74
|
* it is the closest thing this instrument has to a direct observation of a
|
|
75
75
|
* never-returning dispatch, which is why the leak is reported rather than
|
|
76
|
-
* quietly swept. `session_id` rides on the record
|
|
77
|
-
* by session; nothing reads it yet and it is not
|
|
76
|
+
* quietly swept. `session_id` rides on the open record AND on both dispatch
|
|
77
|
+
* lines so a rate can be scoped by session; nothing reads it yet and it is not
|
|
78
|
+
* claimed to.
|
|
78
79
|
*
|
|
79
80
|
* ── Depth and concurrency (Step 3) ────────────────────────────────────────
|
|
80
81
|
*
|
|
@@ -598,6 +599,13 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
|
|
|
598
599
|
event: 'subagent_start',
|
|
599
600
|
ts: nowIso,
|
|
600
601
|
ref,
|
|
602
|
+
// Phase 1 Step 4 correction (b): without this the dispatch denominator
|
|
603
|
+
// is not per-session. Three sessions share one ledger file, so a rate
|
|
604
|
+
// computed over the file aggregates strangers — the measured window
|
|
605
|
+
// held 7 starts against 25 stops, one of them an agent type this
|
|
606
|
+
// session never dispatched. The open record already carried it; the
|
|
607
|
+
// appended line did not, and the line is what a rate is computed from.
|
|
608
|
+
session_id: sessionId,
|
|
601
609
|
agent_type: rec.agent_type,
|
|
602
610
|
parent_ref: parentRef,
|
|
603
611
|
depth,
|
|
@@ -606,7 +614,7 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
|
|
|
606
614
|
});
|
|
607
615
|
}
|
|
608
616
|
|
|
609
|
-
function handleStop(root: string, payload: JsonObject, nowIso: string): void {
|
|
617
|
+
function handleStop(root: string, payload: JsonObject, nowIso: string, sessionId: string | null): void {
|
|
610
618
|
const agentId = str(payload, 'agent_id', 'agentId');
|
|
611
619
|
if (agentId === null) {
|
|
612
620
|
appendUnidentified(root, 'subagent_stop', nowIso, payload);
|
|
@@ -631,6 +639,11 @@ function handleStop(root: string, payload: JsonObject, nowIso: string): void {
|
|
|
631
639
|
event: 'subagent_stop',
|
|
632
640
|
ts: nowIso,
|
|
633
641
|
ref,
|
|
642
|
+
// The session this stop was OBSERVED in, read from the same envelope
|
|
643
|
+
// position as the start's — never back-filled from `rec`, which would
|
|
644
|
+
// relabel a cross-session stop as belonging to the dispatching session
|
|
645
|
+
// and hide exactly the mismatch this field exists to expose.
|
|
646
|
+
session_id: sessionId,
|
|
634
647
|
agent_type: rec?.agent_type ?? str(payload, 'agent_type', 'agentType', 'subagent_type', 'subagentType'),
|
|
635
648
|
depth: rec?.depth ?? null,
|
|
636
649
|
depth_basis: rec?.depth_basis ?? null,
|
|
@@ -661,7 +674,7 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string): numb
|
|
|
661
674
|
reapStaleOpenRecords(consumerRoot, nowIso);
|
|
662
675
|
|
|
663
676
|
if (event === 'subagent_start') handleStart(consumerRoot, payload, nowIso, sessionId);
|
|
664
|
-
else handleStop(consumerRoot, payload, nowIso);
|
|
677
|
+
else handleStop(consumerRoot, payload, nowIso, sessionId);
|
|
665
678
|
} catch {
|
|
666
679
|
// Malformed payload, unreadable disk, anything — never disturb the run.
|
|
667
680
|
return EXIT_ALLOW;
|