@event4u/agent-config 14.7.0 → 14.9.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 +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1358 -805
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.js.map +1 -0
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +134 -21
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
package/src/scripts/condense.ts
CHANGED
|
@@ -263,7 +263,9 @@ export {
|
|
|
263
263
|
type ClaudePathsPlan,
|
|
264
264
|
} from '../install/claudePathsPlan.js';
|
|
265
265
|
import { rule_in_scope } from '../install/ruleInScope.js';
|
|
266
|
-
import {
|
|
266
|
+
import { pruneEmptyDirs } from './_lib/prune_empty_dirs.js';
|
|
267
|
+
import { commandsWithheld, partitionActive, personaPartition, setPartitionAnnounce } from '../install/partitionEligibility.js'; // ADR-236
|
|
268
|
+
import { dedupableRules, partitionRulesForDir } from '../install/ruleLayerPartition.js'; // ADR-236, per-host evidence
|
|
267
269
|
import { _claude_paths_plan, derive_trigger_globs } from '../install/claudePathsPlan.js';
|
|
268
270
|
|
|
269
271
|
const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
|
|
@@ -439,15 +441,6 @@ function _read_augment_rules_use_symlinks(): boolean {
|
|
|
439
441
|
return false;
|
|
440
442
|
}
|
|
441
443
|
|
|
442
|
-
/**
|
|
443
|
-
* Per-tool user-scope rule directory, for the scope-dedup below. Only tools
|
|
444
|
-
* whose host reads a user-scope rules directory can have a redundant twin;
|
|
445
|
-
* everything else has nothing to de-duplicate against.
|
|
446
|
-
*/
|
|
447
|
-
const USER_SCOPE_RULE_DIRS: Readonly<Record<string, string>> = {
|
|
448
|
-
'.claude/rules': path.join('.claude', 'rules'),
|
|
449
|
-
};
|
|
450
|
-
|
|
451
444
|
function _read_projection_scope_dedup(): boolean {
|
|
452
445
|
const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
|
|
453
446
|
const projection = data['projection'];
|
|
@@ -464,60 +457,6 @@ function _read_projection_scope_dedup(): boolean {
|
|
|
464
457
|
return false;
|
|
465
458
|
}
|
|
466
459
|
|
|
467
|
-
/**
|
|
468
|
-
* Rules whose user-scope twin is BYTE-IDENTICAL to the source, i.e. safe to
|
|
469
|
-
* skip at project scope: the host still loads the same text, once instead of
|
|
470
|
-
* twice, so nothing the model sees changes.
|
|
471
|
-
*
|
|
472
|
-
* Byte-identity is the whole safety argument and is not negotiable. Keying on
|
|
473
|
-
* the filename alone would silently let a stale globally-installed copy win
|
|
474
|
-
* whenever the two scopes hold different versions — which is the NORMAL state
|
|
475
|
-
* while developing the package (measured: 110/110 shared filenames differing in
|
|
476
|
-
* bytes). This is deliberately not the thin-projection mechanism: no rule
|
|
477
|
-
* becomes trigger-gated, so the quality floor that disabled thin projection
|
|
478
|
-
* does not apply here.
|
|
479
|
-
*/
|
|
480
|
-
function _dedupable_rules(tool_dir: string, rules: readonly string[], userHome: string): Set<string> {
|
|
481
|
-
const relative = USER_SCOPE_RULE_DIRS[tool_dir];
|
|
482
|
-
if (relative === undefined) {
|
|
483
|
-
return new Set();
|
|
484
|
-
}
|
|
485
|
-
// A hostile or simply absent $HOME must make the dedup inert, not
|
|
486
|
-
// adventurous. In a container `$HOME` is often unset (so `homedir()` can
|
|
487
|
-
// resolve to `/`) or world-writable, and this function decides which rules
|
|
488
|
-
// to STOP emitting — reading an unexpected tree there is how a projection
|
|
489
|
-
// silently loses a rule. Council review of PR #1055 raised exactly this.
|
|
490
|
-
let userDirStat: fs.Stats;
|
|
491
|
-
const userDir = path.join(userHome, relative);
|
|
492
|
-
try {
|
|
493
|
-
userDirStat = fs.statSync(userDir);
|
|
494
|
-
} catch {
|
|
495
|
-
return new Set();
|
|
496
|
-
}
|
|
497
|
-
if (!userDirStat.isDirectory()) {
|
|
498
|
-
return new Set();
|
|
499
|
-
}
|
|
500
|
-
// World-writable user scope: anyone on the box could plant a byte-identical
|
|
501
|
-
// twin and thereby delete a rule from the project projection. Refuse.
|
|
502
|
-
if ((userDirStat.mode & 0o002) !== 0) {
|
|
503
|
-
_print(` ⚠️ ${tool_dir}: user-scope rules dir is world-writable — scope-dedup skipped`);
|
|
504
|
-
return new Set();
|
|
505
|
-
}
|
|
506
|
-
const skip = new Set<string>();
|
|
507
|
-
for (const rule of rules) {
|
|
508
|
-
const twin = path.join(userDir, rule);
|
|
509
|
-
const source = path.join(MODULE_STATE.RULES_SOURCE, rule);
|
|
510
|
-
try {
|
|
511
|
-
if (!_isFile(twin) || !_isFile(source)) continue;
|
|
512
|
-
if (fs.readFileSync(twin).equals(fs.readFileSync(source))) {
|
|
513
|
-
skip.add(rule);
|
|
514
|
-
}
|
|
515
|
-
} catch {
|
|
516
|
-
// An unreadable twin is simply not de-duplicable — emit the project copy.
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return skip;
|
|
520
|
-
}
|
|
521
460
|
|
|
522
461
|
function _lean_projection_mode(): string {
|
|
523
462
|
const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
|
|
@@ -1132,11 +1071,22 @@ function _scoped_rule_basenames(): string[] {
|
|
|
1132
1071
|
// is its fourth consumer, after the router compiler, the dist writer, and
|
|
1133
1072
|
// check_sync. Same predicate in all four; that is the point.
|
|
1134
1073
|
.filter((p) => !skip_compile_disabled_rule(`rules/${path.basename(p)}`))
|
|
1135
|
-
.filter((p) => !partitionActive(MODULE_STATE.PROJECT_ROOT) || isExclusivelyPackageOnly(p))
|
|
1136
1074
|
.map((p) => path.basename(p))
|
|
1137
1075
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1138
1076
|
}
|
|
1139
1077
|
|
|
1078
|
+
/** Adapter over {@link partitionRulesForDir}; that module carries the reasoning. */
|
|
1079
|
+
export function partition_rules_for_dir(
|
|
1080
|
+
tool_dir: string,
|
|
1081
|
+
rules: readonly string[],
|
|
1082
|
+
user_home: string = process.env['HOME'] ?? os.homedir(),
|
|
1083
|
+
): string[] {
|
|
1084
|
+
return partitionRulesForDir({
|
|
1085
|
+
toolDir: tool_dir, rules, userHome: user_home, announce: _print,
|
|
1086
|
+
projectRoot: MODULE_STATE.PROJECT_ROOT, rulesSource: MODULE_STATE.RULES_SOURCE,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1140
1090
|
/**
|
|
1141
1091
|
* The emit plan of {@link generate_rule_symlinks}: active tool dir → the exact
|
|
1142
1092
|
* rule basenames it projects there.
|
|
@@ -1161,8 +1111,12 @@ export function projected_rule_trees(
|
|
|
1161
1111
|
const dedup_on = _read_projection_scope_dedup();
|
|
1162
1112
|
const out: Record<string, string[]> = {};
|
|
1163
1113
|
for (const tool_dir of Object.keys(_filter_tool_dirs(TOOL_DIRS))) {
|
|
1164
|
-
const skip = dedup_on
|
|
1165
|
-
|
|
1114
|
+
const skip = dedup_on
|
|
1115
|
+
? dedupableRules({ toolDir: tool_dir, rules, userHome: user_home,
|
|
1116
|
+
rulesSource: MODULE_STATE.RULES_SOURCE, announce: _print })
|
|
1117
|
+
: new Set<string>();
|
|
1118
|
+
const kept = rules.filter((r) => !skip.has(r));
|
|
1119
|
+
out[tool_dir] = partition_rules_for_dir(tool_dir, kept, user_home);
|
|
1166
1120
|
}
|
|
1167
1121
|
return out;
|
|
1168
1122
|
}
|
|
@@ -1281,7 +1235,10 @@ export function generate_rule_symlinks(): number {
|
|
|
1281
1235
|
}
|
|
1282
1236
|
|
|
1283
1237
|
export function generate_windsurfrules(): number {
|
|
1284
|
-
|
|
1238
|
+
// Windsurf's LEGACY single-file surface — same host, so the same evidence, keyed
|
|
1239
|
+
// on `.windsurf/rules`. Without this line removing the per-run filter regressed
|
|
1240
|
+
// it 13 → 113: the per-run filter had been covering it by accident.
|
|
1241
|
+
const rules = partition_rules_for_dir('.windsurf/rules', _scoped_rule_basenames());
|
|
1285
1242
|
const parts = ['# Auto-generated from dist/agent-src/rules/ — do not edit directly\n'];
|
|
1286
1243
|
for (const rule of rules) {
|
|
1287
1244
|
const p = path.join(MODULE_STATE.RULES_SOURCE, rule);
|
|
@@ -1451,12 +1408,16 @@ function _clean_modern_dir(target_dir: string, valid_names: ReadonlySet<string>)
|
|
|
1451
1408
|
export function generate_cursor_mdc_rules(): number {
|
|
1452
1409
|
const scope = _read_rule_workspaces();
|
|
1453
1410
|
const pack_scope = _read_rule_packs();
|
|
1454
|
-
|
|
1411
|
+
let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
|
|
1455
1412
|
.filter((p) => _isFile(p))
|
|
1456
1413
|
.filter((p) => rule_in_scope(p, scope, pack_scope))
|
|
1457
1414
|
// ADR-004 manual rules are reference-only — never auto-injected into a
|
|
1458
1415
|
// per-tool tree. Same predicate as generate_rule_symlinks.
|
|
1459
1416
|
.filter((p) => !_is_manual_rule(p));
|
|
1417
|
+
// ADR-236 per-host partition (src/install/ruleLayerPartition.ts). Narrowing the
|
|
1418
|
+
// list is a REMOVAL, because `_clean_modern_dir` below sweeps to `valid`.
|
|
1419
|
+
const kept = new Set(partition_rules_for_dir('.cursor/rules', rules.map((r) => path.basename(r))));
|
|
1420
|
+
rules = rules.filter((r) => kept.has(path.basename(r)));
|
|
1460
1421
|
const stems = rules.map((r) => path.basename(r, '.md'));
|
|
1461
1422
|
const valid = new Set([
|
|
1462
1423
|
...stems.map((s) => `${s}.mdc`),
|
|
@@ -1473,12 +1434,15 @@ export function generate_cursor_mdc_rules(): number {
|
|
|
1473
1434
|
export function generate_windsurf_modern_rules(): number {
|
|
1474
1435
|
const scope = _read_rule_workspaces();
|
|
1475
1436
|
const pack_scope = _read_rule_packs();
|
|
1476
|
-
|
|
1437
|
+
let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
|
|
1477
1438
|
.filter((p) => _isFile(p))
|
|
1478
1439
|
.filter((p) => rule_in_scope(p, scope, pack_scope))
|
|
1479
1440
|
// ADR-004 manual rules are reference-only — never auto-injected into a
|
|
1480
1441
|
// per-tool tree. Same predicate as generate_rule_symlinks.
|
|
1481
1442
|
.filter((p) => !_is_manual_rule(p));
|
|
1443
|
+
// ADR-236 per-host partition — same shape as the cursor emitter above.
|
|
1444
|
+
const kept = new Set(partition_rules_for_dir('.windsurf/rules', rules.map((r) => path.basename(r))));
|
|
1445
|
+
rules = rules.filter((r) => kept.has(path.basename(r)));
|
|
1482
1446
|
const valid = new Set(rules.map((r) => path.basename(r)));
|
|
1483
1447
|
_clean_modern_dir(_WINDSURF_RULES_DIR(), valid);
|
|
1484
1448
|
for (const rule of rules) {
|
|
@@ -1781,18 +1745,16 @@ function _nested_command_subpath(source_file: string): string | null {
|
|
|
1781
1745
|
/**
|
|
1782
1746
|
* Project-scope `.claude/commands/<cluster>/<sub>.md` — the colon form.
|
|
1783
1747
|
*
|
|
1784
|
-
* Before this, the package repo's own `.claude/` carried NO commands
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
*
|
|
1790
|
-
* of always-loaded catalog for zero added reach — measured 2026-08-02.
|
|
1748
|
+
* Before this, the package repo's own `.claude/` carried NO commands directory:
|
|
1749
|
+
* every command reached Claude Code as a hyphen-named skill wrapper in
|
|
1750
|
+
* `.claude/skills/`, while the colon form existed only in the user-global tree
|
|
1751
|
+
* written by `install.ts`. Every clustered command was therefore listed TWICE
|
|
1752
|
+
* for anyone with a global install (`roadmap-process-full` AND
|
|
1753
|
+
* `roadmap:process-full`), costing 4,214 GPT tok of catalog — measured 2026-08-02.
|
|
1791
1754
|
*
|
|
1792
1755
|
* Emitting the nested form here lets `generate_claude_commands` stop wrapping
|
|
1793
1756
|
* the clustered commands: one listing, same reachability, and reachability no
|
|
1794
|
-
* longer depends on a global deploy
|
|
1795
|
-
* and user scope by name, so the two copies of `/cluster:sub` collapse.
|
|
1757
|
+
* longer depends on a global deploy. Dedup AND precedence: `commandsWithheld`.
|
|
1796
1758
|
*
|
|
1797
1759
|
* ADR-003 (colon canonical for clusters) and ADR-044 (flat commands stay
|
|
1798
1760
|
* hyphenated) both hold: flat commands are untouched and keep their wrapper.
|
|
@@ -1800,6 +1762,8 @@ function _nested_command_subpath(source_file: string): string | null {
|
|
|
1800
1762
|
export function generate_claude_project_commands(
|
|
1801
1763
|
active_command_slugs: ReadonlySet<string> | null = null,
|
|
1802
1764
|
): number {
|
|
1765
|
+
if (commandsWithheld(MODULE_STATE.PROJECT_ROOT)) active_command_slugs = new Set<string>();
|
|
1766
|
+
// Returns before the stale sweep — invariant, see `commandsWithheld`.
|
|
1803
1767
|
if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
|
|
1804
1768
|
return 0;
|
|
1805
1769
|
}
|
|
@@ -1816,8 +1780,7 @@ export function generate_claude_project_commands(
|
|
|
1816
1780
|
|
|
1817
1781
|
const target_root = _CLAUDE_COMMANDS_DIR();
|
|
1818
1782
|
const valid = new Set(nested.map(([, sub]) => `${sub}.md`));
|
|
1819
|
-
// Sweep stale links first
|
|
1820
|
-
// a dangling `/cluster:sub`.
|
|
1783
|
+
// Sweep stale links first — a renamed, deleted or WITHHELD command must not linger.
|
|
1821
1784
|
if (_exists(target_root)) {
|
|
1822
1785
|
for (const item of _rglobSorted(target_root, '*.md')) {
|
|
1823
1786
|
const rel = _relativeToPosix(item, target_root);
|
|
@@ -1825,6 +1788,7 @@ export function generate_claude_project_commands(
|
|
|
1825
1788
|
fs.unlinkSync(item);
|
|
1826
1789
|
}
|
|
1827
1790
|
}
|
|
1791
|
+
pruneEmptyDirs(target_root); // else the gate counts 40 empty cluster dirs as 40 commands
|
|
1828
1792
|
}
|
|
1829
1793
|
|
|
1830
1794
|
let count = 0;
|
|
@@ -2297,7 +2261,7 @@ export function generate_persona_symlinks(): number {
|
|
|
2297
2261
|
.map((p) => path.basename(p))
|
|
2298
2262
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
2299
2263
|
void personas;
|
|
2300
|
-
const
|
|
2264
|
+
const partition = personaPartition(MODULE_STATE.PROJECT_ROOT, direct);
|
|
2301
2265
|
const tool_dirs = _filter_tool_dirs(PERSONA_TOOL_DIRS);
|
|
2302
2266
|
let total = 0;
|
|
2303
2267
|
for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
|
|
@@ -2305,11 +2269,11 @@ export function generate_persona_symlinks(): number {
|
|
|
2305
2269
|
_mkdirp(target_dir);
|
|
2306
2270
|
for (const item of _iterdirSorted(target_dir)) {
|
|
2307
2271
|
const name = path.basename(item);
|
|
2308
|
-
if (_isSymlink(item) && !
|
|
2272
|
+
if (_isSymlink(item) && !partition.listFor(tool_dir).includes(name) && name !== 'README.md') {
|
|
2309
2273
|
fs.unlinkSync(item);
|
|
2310
2274
|
}
|
|
2311
2275
|
}
|
|
2312
|
-
for (const persona of
|
|
2276
|
+
for (const persona of partition.listFor(tool_dir)) {
|
|
2313
2277
|
const link = path.join(target_dir, persona);
|
|
2314
2278
|
const target = path.join(rel_prefix, persona);
|
|
2315
2279
|
if (_existsOrSymlink(link)) {
|
|
@@ -2319,9 +2283,8 @@ export function generate_persona_symlinks(): number {
|
|
|
2319
2283
|
total += 1;
|
|
2320
2284
|
}
|
|
2321
2285
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
);
|
|
2286
|
+
const per_dir = Object.keys(tool_dirs).map((d) => `${d}=${partition.countFor(d)}`);
|
|
2287
|
+
info(` ✅ Created ${total} persona symlinks — ${per_dir.join(' · ')}${partition.note}`);
|
|
2325
2288
|
return total;
|
|
2326
2289
|
}
|
|
2327
2290
|
|
|
@@ -2521,10 +2484,17 @@ export function project_to_augment(): void {
|
|
|
2521
2484
|
const current = new Set<string>();
|
|
2522
2485
|
let written = 0;
|
|
2523
2486
|
if (_exists(src_rules)) {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2487
|
+
// ADR-236 per-host partition, ahead of the `use_symlinks` branch so both
|
|
2488
|
+
// writers narrow; the `existing` minus `current` sweep below makes it a
|
|
2489
|
+
// removal. This emitter never had the scope or ADR-004 manual filters
|
|
2490
|
+
// either — why its count is 15 and the symlink trees' 13. Pre-existing.
|
|
2491
|
+
const rules = partition_rules_for_dir(
|
|
2492
|
+
'.augment/rules',
|
|
2493
|
+
_iterdirSorted(src_rules)
|
|
2494
|
+
.filter((p) => p.endsWith('.md') && _isFile(p))
|
|
2495
|
+
.map((p) => path.basename(p))
|
|
2496
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
2497
|
+
);
|
|
2528
2498
|
for (const rule of rules) {
|
|
2529
2499
|
const target = path.join(dst_rules, rule);
|
|
2530
2500
|
if (_isSymlink(target) || _existsOrSymlink(target)) {
|
|
@@ -106,7 +106,8 @@ import {
|
|
|
106
106
|
renderPostureLines,
|
|
107
107
|
type FallbackPosture,
|
|
108
108
|
} from './_lib/council_fallback_posture.js';
|
|
109
|
-
import {
|
|
109
|
+
import { wireCouncilFallback } from './_lib/council_fallback_wiring.js';
|
|
110
|
+
import { BILLING_SUBCOMMANDS, billingArgSpec, handleBillingCommand, printBillingGate } from './_lib/billing_grant_cli.js';
|
|
110
111
|
import {
|
|
111
112
|
type ClassificationResult,
|
|
112
113
|
type SizeFitVerdict,
|
|
@@ -880,19 +881,11 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
880
881
|
'prints the resolved path.',
|
|
881
882
|
);
|
|
882
883
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
apiOnQuota: _pyBool(fallback_cfg['api_on_quota']),
|
|
889
|
-
hasApiRung: (p) => _API_PROVIDERS.has(p),
|
|
890
|
-
memberConfig: (p) => ((members_cfg[p] as Dict) || {}) as Dict,
|
|
891
|
-
modelOverride: (p) => (overrides[p] as string | undefined) ?? null,
|
|
892
|
-
constructApi: _construct_api_member,
|
|
893
|
-
emit: appendEvent,
|
|
894
|
-
});
|
|
895
|
-
}
|
|
884
|
+
wireCouncilFallback(fallback_out, ai, {
|
|
885
|
+
repoRoot: REPO_ROOT, isDict: _isDict, membersCfg: members_cfg, overrides,
|
|
886
|
+
hasApiRung: (p) => _API_PROVIDERS.has(p),
|
|
887
|
+
constructApi: _construct_api_member, emit: appendEvent,
|
|
888
|
+
});
|
|
896
889
|
return members;
|
|
897
890
|
}
|
|
898
891
|
|
|
@@ -2668,6 +2661,7 @@ function cmd_run(
|
|
|
2668
2661
|
quorum_out.result = post_run.quorum;
|
|
2669
2662
|
skipped.push(...post_run.absent);
|
|
2670
2663
|
recordRoundObservations(REPO_ROOT, members, responses, classifyCliFailure);
|
|
2664
|
+
printBillingGate(fallback_out.options?.parked ?? [], { repoRoot: REPO_ROOT, stdout: _stdout, stderr: _stderr });
|
|
2671
2665
|
// Attendance telemetry for the reading that actually decided the pass.
|
|
2672
2666
|
// `post_run.absent` is the mid-flight set only; the construction-time
|
|
2673
2667
|
// skips already went out under `phase: 'pre_run'` from `build_members`,
|
|
@@ -3509,7 +3503,7 @@ interface Args {
|
|
|
3509
3503
|
json: boolean;
|
|
3510
3504
|
}
|
|
3511
3505
|
|
|
3512
|
-
const _SUBCOMMANDS = ['estimate', 'run', 'debate', 'render', 'replay', 'quota', 'shadow-report', 'status'];
|
|
3506
|
+
const _SUBCOMMANDS = ['estimate', 'run', 'debate', 'render', 'replay', 'quota', 'shadow-report', 'status', ...BILLING_SUBCOMMANDS];
|
|
3513
3507
|
|
|
3514
3508
|
const _TOP_USAGE =
|
|
3515
3509
|
`usage: ${_PROG} [-h]\n` +
|
|
@@ -3698,7 +3692,7 @@ function _specsFor(cmd: string): { positionals: string[]; opts: OptSpec[]; requi
|
|
|
3698
3692
|
requiredOpts: [],
|
|
3699
3693
|
};
|
|
3700
3694
|
default:
|
|
3701
|
-
return { positionals: [], opts: [], requiredOpts: [] };
|
|
3695
|
+
return billingArgSpec(cmd) ?? { positionals: [], opts: [], requiredOpts: [] };
|
|
3702
3696
|
}
|
|
3703
3697
|
}
|
|
3704
3698
|
|
|
@@ -3902,6 +3896,8 @@ function main(argv: string[] | null = null): number {
|
|
|
3902
3896
|
if (args.cmd === 'status') {
|
|
3903
3897
|
return cmd_status(args);
|
|
3904
3898
|
}
|
|
3899
|
+
const billing = handleBillingCommand(String(args.cmd), _getattr(args, 'run_id', null), { repoRoot: REPO_ROOT, stdout: _stdout, stderr: _stderr });
|
|
3900
|
+
if (billing !== null) return billing;
|
|
3905
3901
|
} catch (exc) {
|
|
3906
3902
|
if (exc instanceof CouncilDisabledError) {
|
|
3907
3903
|
_stderr(`❌ council:${args.cmd}: ${exc.message}\n`);
|
|
@@ -111,6 +111,93 @@ export function scanHiddenUnicode(text: string): HiddenUnicodeFinding[] {
|
|
|
111
111
|
}));
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** One codepoint the strip considered, and what it decided. */
|
|
115
|
+
export interface CarrierRecord {
|
|
116
|
+
/** `U+XXXX` form, so a record is readable without decoding. */
|
|
117
|
+
codepoint: string;
|
|
118
|
+
/** The class `_classify` returned — never a second class list. */
|
|
119
|
+
cls: string;
|
|
120
|
+
/** Codepoint index (not UTF-16 index) in the input. */
|
|
121
|
+
offset: number;
|
|
122
|
+
disposition: "removed" | "preserved";
|
|
123
|
+
/** Why it was preserved. `null` when removed. */
|
|
124
|
+
reason: string | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface CarrierStripResult {
|
|
128
|
+
out: string;
|
|
129
|
+
removed: number;
|
|
130
|
+
preserved: number;
|
|
131
|
+
records: CarrierRecord[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Remove hidden-Unicode carriers from OUTPUT prose — context-aware, opt-in.
|
|
136
|
+
*
|
|
137
|
+
* ## Why this is not `_sanitize`
|
|
138
|
+
*
|
|
139
|
+
* `lint_hidden_unicode._sanitize` drops every `_classify`-flagged codepoint
|
|
140
|
+
* unconditionally and then applies `NFKC`. That is correct for its own callers —
|
|
141
|
+
* it repairs a source FILE — and it is wrong for a deliverable, because
|
|
142
|
+
* `_ZERO_WIDTH` contains `0x200C` and `0x200D`. A blind pass therefore destroys
|
|
143
|
+
* emoji ZWJ sequences and complex-script joiners, which are legitimate content.
|
|
144
|
+
* This function keeps `_classify` as the single source of truth for *what is a
|
|
145
|
+
* candidate* and adds exactly one predicate for *whether to remove it*.
|
|
146
|
+
*
|
|
147
|
+
* ## The predicate
|
|
148
|
+
*
|
|
149
|
+
* A flagged codepoint is removed only when the codepoints on BOTH sides are
|
|
150
|
+
* ASCII (`< 0x80`) or absent. A carrier between two ASCII characters is doing
|
|
151
|
+
* no linguistic work; one adjacent to any non-ASCII character might be a
|
|
152
|
+
* joiner, so it stays.
|
|
153
|
+
*
|
|
154
|
+
* Deliberately conservative, and the asymmetry is the point: the failure mode
|
|
155
|
+
* is a preserved carrier, never a corrupted word. That makes this a HYGIENE
|
|
156
|
+
* pass and not a security control — the injection vector is covered on the
|
|
157
|
+
* INGESTION side by `scanHiddenUnicode`, whose contract is to warn and never
|
|
158
|
+
* strip, and which this function does not touch.
|
|
159
|
+
*
|
|
160
|
+
* Not a detector-evasion tool: it removes invisible characters and alters no
|
|
161
|
+
* visible prose. That exclusion is permanent.
|
|
162
|
+
*/
|
|
163
|
+
export function stripCarrierUnicode(text: string): CarrierStripResult {
|
|
164
|
+
const cps = [...text].map((ch) => ch.codePointAt(0) ?? 0);
|
|
165
|
+
const kept: string[] = [];
|
|
166
|
+
const records: CarrierRecord[] = [];
|
|
167
|
+
let removed = 0;
|
|
168
|
+
let preserved = 0;
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < cps.length; i += 1) {
|
|
171
|
+
const cp = cps[i] as number;
|
|
172
|
+
const cls = _classify(cp);
|
|
173
|
+
if (cls === null) {
|
|
174
|
+
kept.push(String.fromCodePoint(cp));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// `undefined` at either end is "absent", which counts as ASCII-side: a
|
|
178
|
+
// carrier at a boundary has no neighbour to be joining.
|
|
179
|
+
const before = cps[i - 1];
|
|
180
|
+
const after = cps[i + 1];
|
|
181
|
+
const asciiSide = (n: number | undefined): boolean => n === undefined || n < 0x80;
|
|
182
|
+
const strip = asciiSide(before) && asciiSide(after);
|
|
183
|
+
records.push({
|
|
184
|
+
codepoint: `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
185
|
+
cls,
|
|
186
|
+
offset: i,
|
|
187
|
+
disposition: strip ? "removed" : "preserved",
|
|
188
|
+
reason: strip ? null : "adjacent to a non-ASCII codepoint — may be a joiner",
|
|
189
|
+
});
|
|
190
|
+
if (strip) {
|
|
191
|
+
removed += 1;
|
|
192
|
+
} else {
|
|
193
|
+
preserved += 1;
|
|
194
|
+
kept.push(String.fromCodePoint(cp));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { out: kept.join(""), removed, preserved, records };
|
|
199
|
+
}
|
|
200
|
+
|
|
114
201
|
interface Thresholds {
|
|
115
202
|
maxHard: number;
|
|
116
203
|
maxScore: number;
|
|
@@ -698,6 +698,44 @@ function isoSeconds(d: Date): string {
|
|
|
698
698
|
return d.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
/**
|
|
702
|
+
* The return-envelope POINTER — one line-shaped field, delivered on the
|
|
703
|
+
* dominant dispatch path.
|
|
704
|
+
*
|
|
705
|
+
* ## Why a pointer and not the contract
|
|
706
|
+
*
|
|
707
|
+
* 240 chars is not a round number: it is `MAX_RESPONSE_LINE_CHARS`
|
|
708
|
+
* (`_lib/subagent_response.ts`), so the pointer is one line-shaped field by the
|
|
709
|
+
* contract's own measure. Inlining the shape would put a fourth copy of it in
|
|
710
|
+
* the tree, and a fourth copy is how the contract came to have three
|
|
711
|
+
* inconsistent states.
|
|
712
|
+
*
|
|
713
|
+
* ## Why THIS dispatcher
|
|
714
|
+
*
|
|
715
|
+
* `road-to-subagent-envelope-adoption` step 1.1 says the dominant path, "and it
|
|
716
|
+
* does not fan out to both". The dominant path is **not answerable from the
|
|
717
|
+
* ledger** — 1,725 of 1,845 post-split stops carry a null `agent_type`, which is
|
|
718
|
+
* the 8 % start↔stop join rate recorded elsewhere, so no stop can be attributed
|
|
719
|
+
* to a dispatcher. Decided on invocation surface instead: this file is wired
|
|
720
|
+
* into `/create-pr` and read by three gates (`check_completion_review`,
|
|
721
|
+
* `lint_evidence_artifacts`, `probe_review_binding_drift`), while
|
|
722
|
+
* `team_dispatch` sits behind an ai_team host flag and one `/team delegate`
|
|
723
|
+
* command. It is also the leg with ZERO prior contract hits, so the dominant
|
|
724
|
+
* path and the absent one are the same file.
|
|
725
|
+
*
|
|
726
|
+
* ## What it does not change
|
|
727
|
+
*
|
|
728
|
+
* The findings table stays the artefact — the gates read the file, not the
|
|
729
|
+
* envelope. Per the four-boundary separation in the response contract, this
|
|
730
|
+
* touches the DELIVERY boundary only: what the final message is. It adds no
|
|
731
|
+
* lifecycle status and does not alter the body schema.
|
|
732
|
+
*/
|
|
733
|
+
export const RETURN_ENVELOPE_POINTER =
|
|
734
|
+
'Final message = the return envelope and nothing else: ' +
|
|
735
|
+
'{summary, handoff, confidence, findings, risks}. Shape + the ' +
|
|
736
|
+
'write-to-disk-first rule: contexts/execution/subagent-response-contract.md. ' +
|
|
737
|
+
'The findings table stays a file.';
|
|
738
|
+
|
|
701
739
|
function reviewerPrompt(args: {
|
|
702
740
|
slug: string;
|
|
703
741
|
headSha: string;
|
|
@@ -781,6 +819,10 @@ function reviewerPrompt(args: {
|
|
|
781
819
|
`**Honest-null:** 0 findings, scope ${args.scopeHash}, reviewed <YYYY-MM-DD>`,
|
|
782
820
|
'```',
|
|
783
821
|
'',
|
|
822
|
+
'## Return channel',
|
|
823
|
+
'',
|
|
824
|
+
RETURN_ENVELOPE_POINTER,
|
|
825
|
+
'',
|
|
784
826
|
].join('\n');
|
|
785
827
|
}
|
|
786
828
|
|
|
@@ -268,6 +268,45 @@ function _getStr(d: Dict, key: string, dflt: string): string {
|
|
|
268
268
|
return String(v);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* What the runtime hooks actually DO, in four states and no others.
|
|
273
|
+
*
|
|
274
|
+
* The four: `detects` (it looks and reports nothing a host reads), `warns` (it
|
|
275
|
+
* reports through the host, and the host may ignore it), `blocks` (it can
|
|
276
|
+
* refuse), and `not-enforceable-on-host` (the slot does not exist there).
|
|
277
|
+
*
|
|
278
|
+
* WHY THIS BLOCK EXISTS. `CAPABILITIES.yaml` carried no prompt-injection entry
|
|
279
|
+
* at all, and saying nothing is a different defect from saying too much: a
|
|
280
|
+
* reader looking for injection coverage found absence, which reads as "not
|
|
281
|
+
* covered" rather than as "a warn-only scanner exists and ships off". This is
|
|
282
|
+
* the constructive half of a source proposal whose other half — "remove claims
|
|
283
|
+
* that equate a warn-hook with prevention" — had nothing to remove.
|
|
284
|
+
*
|
|
285
|
+
* `blocks` is never legal for the injection scanner on ANY host, and the value
|
|
286
|
+
* is DERIVED from `hook_manifest.yaml` rather than typed here: `fail_closed:
|
|
287
|
+
* false` plus `severity: advisory` is what makes it `warns`, so a manifest edit
|
|
288
|
+
* that gave it teeth would have to change this row too. A hand-written `blocks`
|
|
289
|
+
* would be the coverage inflation `untrusted-input-defense` refuses, one file
|
|
290
|
+
* over.
|
|
291
|
+
*/
|
|
292
|
+
function _runtime_enforcement_lines(): string[] {
|
|
293
|
+
return [
|
|
294
|
+
'runtime_enforcement:',
|
|
295
|
+
' # Four states, and no others: detects | warns | blocks | not-enforceable-on-host.',
|
|
296
|
+
' # Derived from src/scripts/hook_manifest.yaml (fail_closed + severity), never',
|
|
297
|
+
' # hand-written — a manifest row that gained teeth would have to change this one.',
|
|
298
|
+
' - hook: injection-scan',
|
|
299
|
+
' slot: post_tool_use',
|
|
300
|
+
' state: warns',
|
|
301
|
+
' default: off',
|
|
302
|
+
' blocks_legal_on_any_host: false',
|
|
303
|
+
' reason: ' +
|
|
304
|
+
_scalar(
|
|
305
|
+
'fail_closed: false + severity: advisory in hook_manifest.yaml — it reports through the host and cannot refuse, so it warns and never blocks. Ships enabled: false; the detector was measured at 99.00% recall and a 0.85% false-positive rate over internal/bench/corpora/encoding-channels/ before any default flip is even discussable.<!-- claim:injection-scan-corpus-rates -->',
|
|
306
|
+
),
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
271
310
|
export function build(): string {
|
|
272
311
|
const packs = _load_packs();
|
|
273
312
|
const skillMap = _skill_packs();
|
|
@@ -360,6 +399,8 @@ export function build(): string {
|
|
|
360
399
|
}
|
|
361
400
|
}
|
|
362
401
|
lines.push('');
|
|
402
|
+
lines.push(..._runtime_enforcement_lines());
|
|
403
|
+
lines.push('');
|
|
363
404
|
lines.push('gaps:');
|
|
364
405
|
if (gaps.length > 0) {
|
|
365
406
|
const sortedGaps = [...gaps].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
@@ -153,6 +153,19 @@ export type GitOp =
|
|
|
153
153
|
| "branch"
|
|
154
154
|
| "pr-create"
|
|
155
155
|
| "pr-merge"
|
|
156
|
+
/**
|
|
157
|
+
* Enabling auto-merge. Its OWN op, not a plain merge and not nothing.
|
|
158
|
+
*
|
|
159
|
+
* Irreversible in the same sense a merge is: it commits the outcome to a
|
|
160
|
+
* condition the agent does not control, and the merge then happens with
|
|
161
|
+
* nobody in the loop. Before this split, `gh pr merge --auto` classified
|
|
162
|
+
* identically to a plain merge (the pattern is `\bgh\s+pr\s+merge\b`, and
|
|
163
|
+
* `--auto` is invisible to it), and the GraphQL mutation classified as
|
|
164
|
+
* NOTHING — `ghApiWrite()` needs a REST path and a write method, and
|
|
165
|
+
* `gh api graphql -f query='mutation{enablePullRequestAutoMerge...}'` has
|
|
166
|
+
* neither.
|
|
167
|
+
*/
|
|
168
|
+
| "pr-merge-auto"
|
|
156
169
|
| "tag"
|
|
157
170
|
| "release"
|
|
158
171
|
| "publish";
|
|
@@ -163,6 +176,7 @@ export const ALL_OPS: readonly GitOp[] = [
|
|
|
163
176
|
"branch",
|
|
164
177
|
"pr-create",
|
|
165
178
|
"pr-merge",
|
|
179
|
+
"pr-merge-auto",
|
|
166
180
|
"tag",
|
|
167
181
|
"release",
|
|
168
182
|
"publish",
|
|
@@ -185,8 +199,29 @@ const PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
185
199
|
op: "pr-create",
|
|
186
200
|
re: /\b(erstell(e|en)?|mach(e|en)?|leg(e|en)?\s+an|(er)?(ö|oe)ffne|open|create|raise|aufmachen)\b[^.\n]{0,30}\b(pr|pull[- ]request)\b|\b(pr|pull[- ]request)\b[^.\n]{0,20}\b(erstellen|aufmachen|anlegen|(er)?(ö|oe)ffnen)\b/i,
|
|
187
201
|
},
|
|
188
|
-
// `merge` as an ACTION, never as the noun in "merge conflict" / "merge commit"
|
|
189
|
-
|
|
202
|
+
// `merge` as an ACTION, never as the noun in "merge conflict" / "merge commit",
|
|
203
|
+
// and never under a NEGATION.
|
|
204
|
+
//
|
|
205
|
+
// The negation half closes a measured defect: probed directly against
|
|
206
|
+
// `classifyAuthorization`, "nicht mergen", "don't merge this" and
|
|
207
|
+
// "never auto-merge" each returned ["pr-merge"] — a prompt saying DO NOT
|
|
208
|
+
// MERGE authorized a merge. The noun-sense lookahead was never a negator.
|
|
209
|
+
//
|
|
210
|
+
// The lookBEHIND is line-scoped by `[^.!?\n]` on purpose, the same scoping
|
|
211
|
+
// `turn_end_gate_hook.ts` uses for its own negation exclusion (`:330`) and
|
|
212
|
+
// its negated-claim window (`:490-493`). A sentence boundary ends a
|
|
213
|
+
// negation's reach: "Do not push. Merge PR #12." is two instructions, and a
|
|
214
|
+
// guard that let the first suppress the second would silently stop
|
|
215
|
+
// authorizing merges the user DID order — the failure mode that is worse
|
|
216
|
+
// than the defect, because nothing happens and nothing says why.
|
|
217
|
+
//
|
|
218
|
+
// Reusing that vocabulary rather than inventing a third is deliberate: two
|
|
219
|
+
// negation vocabularies in one tree drift, and the drift is invisible until
|
|
220
|
+
// a prompt lands in the gap between them.
|
|
221
|
+
{
|
|
222
|
+
op: "pr-merge",
|
|
223
|
+
re: /(?<!\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b[^.!?\n]{0,30})\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i,
|
|
224
|
+
},
|
|
190
225
|
// A tag is an ACTION here — bare "Tag" is the German word for day, and
|
|
191
226
|
// "Version" is an ordinary noun. Both authorized a BLOCK op before this.
|
|
192
227
|
{ op: "tag", re: /\b(tagge(n|st)?|tag\s+(setzen|anlegen|erstellen)|git\s+tag|--tags|--follow-tags)\b/i },
|
|
@@ -267,10 +302,20 @@ export function isAffirmative(prose: string): boolean {
|
|
|
267
302
|
}
|
|
268
303
|
|
|
269
304
|
/** Executable commands a user may paste, mapped to the op they authorize. */
|
|
270
|
-
const PASTED_COMMANDS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
305
|
+
const PASTED_COMMANDS: ReadonlyArray<{ op: GitOp | null; re: RegExp }> = [
|
|
271
306
|
{ op: "publish", re: /\bnpm\s+publish\b/i },
|
|
272
307
|
{ op: "tag", re: /\bgit\s+push\s+[^\n]*--tags\b|\bgit\s+tag\s+-a\b/i },
|
|
273
308
|
{ op: "release", re: /\bgh\s+release\s+create\b/i },
|
|
309
|
+
// ORDER IS LOAD-BEARING: the auto variants precede the plain merge, because
|
|
310
|
+
// the first match wins and `gh pr merge 12 --auto` also matches the plain
|
|
311
|
+
// pattern. Same rule the block-side table states for `git push --tags`.
|
|
312
|
+
//
|
|
313
|
+
// The DE-ESCALATING forms come first of all, and they map to no op at all:
|
|
314
|
+
// `--disable-auto` and `disablePullRequestAutoMerge` turn the capability OFF.
|
|
315
|
+
// Requiring merge authorization to switch auto-merge off is a live deadlock,
|
|
316
|
+
// and it is why this cannot ship after the split rather than with it.
|
|
317
|
+
{ op: null, re: /\bgh\s+pr\s+merge\b[^\n]*--disable-auto\b|\bdisablePullRequestAutoMerge\b/i },
|
|
318
|
+
{ op: "pr-merge-auto", re: /\bgh\s+pr\s+merge\b[^\n]*--auto\b|\benablePullRequestAutoMerge\b/i },
|
|
274
319
|
{ op: "pr-merge", re: /\bgh\s+pr\s+merge\b/i },
|
|
275
320
|
{ op: "pr-create", re: /\bgh\s+pr\s+create\b/i },
|
|
276
321
|
{ op: "push", re: /\bgit\s+push\b/i },
|
|
@@ -355,10 +400,15 @@ export function classifyAuthorization(prompt: string): {
|
|
|
355
400
|
}
|
|
356
401
|
for (const line of _commandLines(fence)) {
|
|
357
402
|
for (const { op, re } of PASTED_COMMANDS) {
|
|
358
|
-
if (re.test(line))
|
|
403
|
+
if (!re.test(line)) continue;
|
|
404
|
+
// FIRST MATCH WINS, and the table is ordered most-specific-first. A
|
|
405
|
+
// de-escalating form carries `op: null` — it matched, so no later
|
|
406
|
+
// pattern may claim the line, and it authorizes nothing.
|
|
407
|
+
if (op !== null) {
|
|
359
408
|
authorized.add(op);
|
|
360
409
|
evidence[op] = `pasted command: "${line.slice(0, 80)}"`;
|
|
361
410
|
}
|
|
411
|
+
break;
|
|
362
412
|
}
|
|
363
413
|
}
|
|
364
414
|
}
|