@event4u/agent-config 15.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- 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 +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -49,13 +49,20 @@ import * as path from 'node:path';
|
|
|
49
49
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
50
50
|
|
|
51
51
|
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} from './_lib/rule_injection.js';
|
|
52
|
+
corpusShapeProblems,
|
|
53
|
+
slotFireSizes as sampleSlotFireSizes,
|
|
54
|
+
type SlotFireSizes,
|
|
55
|
+
} from './_lib/activation_payload.js';
|
|
56
|
+
import { loadRouter, pathOnlyRuleIds } from './_lib/rule_injection.js';
|
|
57
57
|
import { CAP_BYTES } from './hooks/rule_inject_hook.js';
|
|
58
58
|
|
|
59
|
+
// Re-exported rather than redefined: this module was their only home until the
|
|
60
|
+
// payload gate needed the same sample, and two copies is how two numbers start
|
|
61
|
+
// disagreeing. `corpusShapeProblems` and `SlotFireSizes` are imported above
|
|
62
|
+
// because this module's own code uses them; these two it only passes through.
|
|
63
|
+
export { percentile, readCorpusPositives } from './_lib/activation_payload.js';
|
|
64
|
+
export { corpusShapeProblems, type SlotFireSizes };
|
|
65
|
+
|
|
59
66
|
const _HERE = fileURLToPath(import.meta.url);
|
|
60
67
|
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
61
68
|
export const ARTIFACT_REL = path.join(
|
|
@@ -149,8 +156,13 @@ export const HOST_SURFACES: readonly HostSurface[] = [
|
|
|
149
156
|
},
|
|
150
157
|
{
|
|
151
158
|
host: 'codex', surface: '.codex/agent-config.md', perRuleTree: false,
|
|
152
|
-
writer: 'src/scripts/install.ts:
|
|
153
|
-
|
|
159
|
+
writer: 'src/scripts/install.ts:1432',
|
|
160
|
+
// Scoped to `.codex` rather than the bare filename: nine other lines in
|
|
161
|
+
// `install.ts` write an `agent-config.md` for some other host, so the
|
|
162
|
+
// unscoped anchor would have accepted a drift onto any of them — the
|
|
163
|
+
// bounds-test-wearing-a-provenance-test's-name failure this field exists
|
|
164
|
+
// to close, one level down.
|
|
165
|
+
anchor: '\'.codex\', \'agent-config.md\'',
|
|
154
166
|
note: 'written by the installer, not by `condense`; absent in this checkout',
|
|
155
167
|
},
|
|
156
168
|
];
|
|
@@ -431,155 +443,17 @@ export function renderArtifact(root: string, pin: string): string {
|
|
|
431
443
|
return L.join('\n');
|
|
432
444
|
}
|
|
433
445
|
|
|
434
|
-
/** One positive prompt of the frozen corpus, with the open files it declares. */
|
|
435
|
-
interface CorpusPrompt {
|
|
436
|
-
prompt: string;
|
|
437
|
-
openFiles: string[] | null;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
446
|
/**
|
|
441
|
-
*
|
|
447
|
+
* The activation-charge sample at the concern's own cap.
|
|
442
448
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* measurement that governs a budget must fail loudly on a shape it cannot read
|
|
449
|
-
* rather than under-count, so the CLI refuses on a non-empty result here.
|
|
450
|
-
*
|
|
451
|
-
* Reported as a shape problem rather than fixed by widening the parser: this
|
|
452
|
-
* script must not grow a second YAML implementation, and the corpus is frozen,
|
|
453
|
-
* so the honest answer to an unreadable shape is to say which file carries it.
|
|
454
|
-
*/
|
|
455
|
-
export function corpusShapeProblems(root: string): string[] {
|
|
456
|
-
const dir = path.join(root, 'tests', 'eval', 'routing-matrix');
|
|
457
|
-
const problems: string[] = [];
|
|
458
|
-
if (!fs.existsSync(dir)) return problems;
|
|
459
|
-
for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
|
|
460
|
-
let inPositives = false;
|
|
461
|
-
let lineNo = 0;
|
|
462
|
-
for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
|
|
463
|
-
lineNo += 1;
|
|
464
|
-
if (/^positives:/u.test(raw)) { inPositives = true; continue; }
|
|
465
|
-
if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
|
|
466
|
-
if (!inPositives) continue;
|
|
467
|
-
if (/^\s*-\s*prompt:/u.test(raw) && !/^\s*-\s*prompt:\s*"(.*)"\s*$/u.test(raw)) {
|
|
468
|
-
problems.push(
|
|
469
|
-
`${f}:${String(lineNo)}: a \`- prompt:\` this parser cannot read — only a ` +
|
|
470
|
-
`double-quoted same-line scalar is accepted: ${JSON.stringify(raw.trim())}`,
|
|
471
|
-
);
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (/^\s*-\s*/u.test(raw) && !/^\s*-\s*(prompt|command|open_files):/u.test(raw)
|
|
475
|
-
&& raw.trim() !== '-' && raw.trim() !== '') {
|
|
476
|
-
// A bare sequence item under `positives:` is a shape with no
|
|
477
|
-
// `prompt:` key on its first line — folded or nested.
|
|
478
|
-
if (!/^\s{4,}/u.test(raw)) {
|
|
479
|
-
problems.push(
|
|
480
|
-
`${f}:${String(lineNo)}: a positives entry whose first line carries no ` +
|
|
481
|
-
`\`prompt:\` key: ${JSON.stringify(raw.trim())}`,
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (/^\s*open_files:\s*$/u.test(raw)) {
|
|
486
|
-
problems.push(
|
|
487
|
-
`${f}:${String(lineNo)}: a BLOCK-sequence \`open_files:\` — only the inline ` +
|
|
488
|
-
'`[...]` form is read, so this entry\'s files would be dropped',
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return problems;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/** Every positive prompt in the frozen routing-matrix corpus. Near-misses are not fires. */
|
|
497
|
-
export function readCorpusPositives(root: string): CorpusPrompt[] {
|
|
498
|
-
const dir = path.join(root, 'tests', 'eval', 'routing-matrix');
|
|
499
|
-
const out: CorpusPrompt[] = [];
|
|
500
|
-
if (!fs.existsSync(dir)) return out;
|
|
501
|
-
for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
|
|
502
|
-
let inPositives = false;
|
|
503
|
-
let cur: CorpusPrompt | null = null;
|
|
504
|
-
for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
|
|
505
|
-
if (/^positives:/u.test(raw)) { inPositives = true; continue; }
|
|
506
|
-
if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
|
|
507
|
-
if (!inPositives) continue;
|
|
508
|
-
const p = /^\s*-\s*prompt:\s*"(.*)"\s*$/u.exec(raw);
|
|
509
|
-
if (p !== null) {
|
|
510
|
-
cur = { prompt: p[1] as string, openFiles: null };
|
|
511
|
-
out.push(cur);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
const of = /^\s*open_files:\s*\[(.*)\]\s*$/u.exec(raw);
|
|
515
|
-
if (of !== null && cur !== null) {
|
|
516
|
-
cur.openFiles = (of[1] as string)
|
|
517
|
-
.split(',')
|
|
518
|
-
.map((x) => x.trim().replace(/^["']|["']$/gu, ''))
|
|
519
|
-
.filter((x) => x !== '');
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return out;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** Nearest-rank percentile over an unsorted sample. Empty sample → 0. */
|
|
527
|
-
export function percentile(xs: readonly number[], q: number): number {
|
|
528
|
-
if (xs.length === 0) return 0;
|
|
529
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
530
|
-
return s[Math.min(s.length - 1, Math.floor(q * (s.length - 1)))] as number;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
export interface SlotFireSizes {
|
|
534
|
-
readonly slot: string;
|
|
535
|
-
readonly fires: number;
|
|
536
|
-
readonly p50: number;
|
|
537
|
-
readonly p90: number;
|
|
538
|
-
readonly max: number;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Gate-open per-fire emission sizes for the delivery concern, per slot
|
|
543
|
-
* (road-to-delivery-for-every-host 3.1).
|
|
544
|
-
*
|
|
545
|
-
* Measured through the SAME selection the runtime concern uses —
|
|
546
|
-
* `matchTierRules` + `selectForInjection` at the concern's own `CAP_BYTES` —
|
|
547
|
-
* rather than through a second model of it. An offline figure computed by a
|
|
548
|
-
* different matcher would price a set the concern does not deliver.
|
|
549
|
-
*
|
|
550
|
-
* `pre_compact` is 0 by construction rather than by measurement: that branch of
|
|
551
|
-
* `rule_inject_hook` clears the seen-set and returns allow without writing to
|
|
552
|
-
* stdout, so there is no emission to sample.
|
|
449
|
+
* The sampling itself moved to `_lib/activation_payload.ts` when the payload
|
|
450
|
+
* gate needed the same figure (`road-to-the-delivery-flip-that-tells-the-truth`
|
|
451
|
+
* 3.1). This wrapper supplies `CAP_BYTES` so the report's own callers keep the
|
|
452
|
+
* one-argument form, and the helpers below are re-exported so there is still
|
|
453
|
+
* exactly one implementation of each.
|
|
553
454
|
*/
|
|
554
455
|
export function slotFireSizes(root: string): SlotFireSizes[] {
|
|
555
|
-
|
|
556
|
-
const prompts = readCorpusPositives(root);
|
|
557
|
-
const sample = (mode: 'prompt' | 'file'): number[] => {
|
|
558
|
-
const out: number[] = [];
|
|
559
|
-
for (const r of prompts) {
|
|
560
|
-
if (mode === 'file' && (r.openFiles === null || r.openFiles.length === 0)) continue;
|
|
561
|
-
const matches =
|
|
562
|
-
mode === 'prompt'
|
|
563
|
-
? matchTierRules(router, r.prompt, null, null)
|
|
564
|
-
: matchTierRules(router, '', r.openFiles, null);
|
|
565
|
-
if (matches.length === 0) continue;
|
|
566
|
-
const sel = selectForInjection(root, matches, CAP_BYTES);
|
|
567
|
-
if (sel.selected.length > 0) out.push(sel.bytes);
|
|
568
|
-
}
|
|
569
|
-
return out;
|
|
570
|
-
};
|
|
571
|
-
const row = (slot: string, xs: number[]): SlotFireSizes => ({
|
|
572
|
-
slot,
|
|
573
|
-
fires: xs.length,
|
|
574
|
-
p50: percentile(xs, 0.5),
|
|
575
|
-
p90: percentile(xs, 0.9),
|
|
576
|
-
max: xs.length === 0 ? 0 : Math.max(...xs),
|
|
577
|
-
});
|
|
578
|
-
return [
|
|
579
|
-
row('user_prompt_submit', sample('prompt')),
|
|
580
|
-
row('pre_tool_use', sample('file')),
|
|
581
|
-
row('pre_compact', []),
|
|
582
|
-
];
|
|
456
|
+
return sampleSlotFireSizes(root, CAP_BYTES);
|
|
583
457
|
}
|
|
584
458
|
|
|
585
459
|
function headSha(root: string): string {
|
|
@@ -101,6 +101,29 @@
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
|
+
"delivery": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"additionalProperties": true,
|
|
107
|
+
"properties": {
|
|
108
|
+
"merge": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"enum": ["off", "on-green"],
|
|
111
|
+
"default": "off",
|
|
112
|
+
"description": "Whether a run may merge the pull request it produced. off (default) = the run ends at mergeable-green-and-open and says so. on-green = merge is permitted, and only when an object-bound grant covering {op: prod_merge, target: <base>} exists and the final head is required-check-green, target-current and tamper-checked (ADR-268 section 3). A precondition, never the authorisation by itself; forge auto-merge is the mechanism, so branch protection stays the gate."
|
|
113
|
+
},
|
|
114
|
+
"wait_for_ci": {
|
|
115
|
+
"type": "boolean",
|
|
116
|
+
"default": true,
|
|
117
|
+
"description": "Whether the run stays alive until the required checks settle. true (default) = the run drives CI and reports the settled verdict. false = the run ends once the branch is pushed and the PR is open. Independent of merge: a run may wait for CI without being allowed to merge."
|
|
118
|
+
},
|
|
119
|
+
"pr_topology": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"enum": ["single", "stacked"],
|
|
122
|
+
"default": "single",
|
|
123
|
+
"description": "How a mission's work is shaped into pull requests. single (default) = one branch, one pull request. stacked = a dependent series. stacked is never chosen by the agent and never asked about at roadmap creation — the owner plans it in roadmap frontmatter or it does not happen."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
104
127
|
"screenshots": {
|
|
105
128
|
"type": "object",
|
|
106
129
|
"additionalProperties": true,
|
|
@@ -103,6 +103,10 @@
|
|
|
103
103
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
104
104
|
"description": "Sub-command identifier within the cluster (e.g. `ci` for `/fix ci`)."
|
|
105
105
|
},
|
|
106
|
+
"produces_roadmap": {
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"description": "This command produces a roadmap, so it ends in the plan-closure pass (`/challenge-me closure`). The declaration replaces the hard-coded entrance list that lived inside three command bodies and left the other five producers uncovered with nothing saying so. `lint_roadmap_producers` reds a command that declares it and carries no reference to the closure pass. Omit the key for commands that produce no plan."
|
|
109
|
+
},
|
|
106
110
|
"superseded_by": {
|
|
107
111
|
"type": "string",
|
|
108
112
|
"pattern": "^[a-z][a-z0-9-]*( (--[a-z][a-z0-9-]*|[a-z][a-z0-9-]*))?$",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Who owns a piece of scope growth discovered mid-mission.
|
|
4
|
+
*
|
|
5
|
+
* The size question — *is this small and aligned enough to do now* — already
|
|
6
|
+
* has an answer in the remediation ladder. This is the other axis, and the two
|
|
7
|
+
* are independent: something small and council-owned is still not the agent's
|
|
8
|
+
* to decide, and something agent-owned and large is still bounded by the
|
|
9
|
+
* smallest-diff rule.
|
|
10
|
+
*
|
|
11
|
+
* Four verdicts, and the last one is the important one:
|
|
12
|
+
*
|
|
13
|
+
* `agent` done inline, recorded as a scope delta in the PR body, never
|
|
14
|
+
* asked about.
|
|
15
|
+
* `council` routed through the ownership ladder; the verdict becomes a
|
|
16
|
+
* `## Decisions` row. It does NOT reach the owner, however
|
|
17
|
+
* consequential — a technical decision does not become
|
|
18
|
+
* owner-owned because it is hard.
|
|
19
|
+
* `owner` the work changes what the product or the business does, or it
|
|
20
|
+
* needs a typed op for which no grant exists. Two rows, closed.
|
|
21
|
+
* `follow-up` not scope growth at all. A larger unrelated opportunity
|
|
22
|
+
* becomes its own artifact and the mission does not expand to
|
|
23
|
+
* hold it — the failure that looks like diligence is a mission
|
|
24
|
+
* that absorbs every good idea it meets and delivers none.
|
|
25
|
+
*
|
|
26
|
+
* `unknown` is a real verdict and is deliberately NOT folded into `agent`:
|
|
27
|
+
* growth nothing classifies is growth nobody decided, and defaulting it to the
|
|
28
|
+
* cheapest rung is how an owner question becomes an agent one silently.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 classified · 1 unknown · 2 bad argv.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
import process from 'node:process';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
39
|
+
const _PROG = 'scope_growth';
|
|
40
|
+
|
|
41
|
+
export type Verdict = 'agent' | 'council' | 'owner' | 'follow-up' | 'unknown';
|
|
42
|
+
|
|
43
|
+
export interface Rule {
|
|
44
|
+
readonly verdict: Verdict;
|
|
45
|
+
readonly kind: string;
|
|
46
|
+
readonly re: RegExp;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ordered, and the order is the contract: `owner` and `follow-up` are tested
|
|
51
|
+
* BEFORE `agent`, so a row that matches both ("a missing test — in a subsystem
|
|
52
|
+
* this mission never touched") resolves to the more conservative rung rather
|
|
53
|
+
* than to the first cheap match.
|
|
54
|
+
*/
|
|
55
|
+
export const RULES: readonly Rule[] = [
|
|
56
|
+
// --- owner: two rows, closed ------------------------------------------
|
|
57
|
+
{
|
|
58
|
+
verdict: 'owner',
|
|
59
|
+
kind: 'product-or-business-change',
|
|
60
|
+
re: /\b(?:new user-visible|changes what the product|changes what the business|a price|pricing|a public commitment|a policy change)\b/i,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
verdict: 'owner',
|
|
64
|
+
kind: 'typed-op-without-grant',
|
|
65
|
+
re: /\b(?:needs a typed op|no grant exists|merge to main|force-push|deploy to prod(?:uction)?|rotate the secret)\b/i,
|
|
66
|
+
},
|
|
67
|
+
// --- follow-up: unrelated, however good -------------------------------
|
|
68
|
+
{
|
|
69
|
+
verdict: 'follow-up',
|
|
70
|
+
kind: 'larger-unrelated',
|
|
71
|
+
re: /\b(?:unrelated|another subsystem|a subsystem this .{0,30}never touched|out of scope|deserves its own)\b/i,
|
|
72
|
+
},
|
|
73
|
+
// --- council: technical, contested ------------------------------------
|
|
74
|
+
{
|
|
75
|
+
verdict: 'council',
|
|
76
|
+
kind: 'larger-internal-re-cut',
|
|
77
|
+
re: /\b(?:re-?cut|restructure the module|reshape the boundary)\b/i,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
verdict: 'council',
|
|
81
|
+
kind: 'two-equal-strategies',
|
|
82
|
+
re: /\b(?:two equal|two valid|either strategy|no evidence between them)\b/i,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
verdict: 'council',
|
|
86
|
+
kind: 'risky-compatibility-design',
|
|
87
|
+
re: /\b(?:compatibility risk|backwards[- ]incompat|breaking change)\b/i,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
verdict: 'council',
|
|
91
|
+
kind: 'unclear-boundary',
|
|
92
|
+
re: /\b(?:unclear boundary|where the boundary)\b/i,
|
|
93
|
+
},
|
|
94
|
+
// --- agent: six rows, closed ------------------------------------------
|
|
95
|
+
{
|
|
96
|
+
verdict: 'agent',
|
|
97
|
+
kind: 'necessary-internal-refactor',
|
|
98
|
+
re: /\b(?:necessary (?:internal )?refactor|cannot land (?:correctly )?without)\b/i,
|
|
99
|
+
},
|
|
100
|
+
{ verdict: 'agent', kind: 'missing-test', re: /\bmissing test\b/i },
|
|
101
|
+
{
|
|
102
|
+
verdict: 'agent',
|
|
103
|
+
kind: 'regression-on-touched-path',
|
|
104
|
+
re: /\bregression\b[^.]{0,60}\btouched\b|\btouched\b[^.]{0,60}\bregression\b/i,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
verdict: 'agent',
|
|
108
|
+
kind: 'small-dependency-adjustment',
|
|
109
|
+
re: /\b(?:small dependency adjustment|patch bump|minor bump)\b/i,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
verdict: 'agent',
|
|
113
|
+
kind: 'local-api-inside-defined-semantics',
|
|
114
|
+
re: /\b(?:inside (?:the )?(?:already-)?defined semantics|the contract does not move)\b/i,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
verdict: 'agent',
|
|
118
|
+
kind: 'boy-scout-cleanup',
|
|
119
|
+
re: /\bboy[- ]scout\b/i,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
export interface Classification {
|
|
124
|
+
readonly verdict: Verdict;
|
|
125
|
+
readonly kind: string;
|
|
126
|
+
/** True when the verdict is done inline and recorded rather than asked. */
|
|
127
|
+
readonly records_scope_delta: boolean;
|
|
128
|
+
/** True when a person is reached at all. */
|
|
129
|
+
readonly asks: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function classifyGrowth(text: string): Classification {
|
|
133
|
+
for (const r of RULES) {
|
|
134
|
+
if (r.re.test(text)) {
|
|
135
|
+
return {
|
|
136
|
+
verdict: r.verdict,
|
|
137
|
+
kind: r.kind,
|
|
138
|
+
records_scope_delta: r.verdict === 'agent',
|
|
139
|
+
asks: r.verdict === 'owner',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { verdict: 'unknown', kind: 'unclassified', records_scope_delta: false, asks: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _usage(): string {
|
|
147
|
+
return `usage: ${_PROG} [-h] [--self-test] [--json] <description...>\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function main(argv?: readonly string[]): number {
|
|
151
|
+
const args = argv ?? process.argv.slice(2);
|
|
152
|
+
let json = false;
|
|
153
|
+
const words: string[] = [];
|
|
154
|
+
for (const a of args) {
|
|
155
|
+
if (a === '-h' || a === '--help') {
|
|
156
|
+
process.stdout.write(_usage());
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
if (a === '--self-test') return _selfTest();
|
|
160
|
+
if (a === '--json') {
|
|
161
|
+
json = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
words.push(a);
|
|
165
|
+
}
|
|
166
|
+
if (!words.length) {
|
|
167
|
+
process.stderr.write(`${_usage()}${_PROG}: error: a description is required\n`);
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
const c = classifyGrowth(words.join(' '));
|
|
171
|
+
if (json) {
|
|
172
|
+
process.stdout.write(JSON.stringify(c, null, 2) + '\n');
|
|
173
|
+
} else {
|
|
174
|
+
process.stdout.write(
|
|
175
|
+
`${c.verdict} (${c.kind}) scope-delta=${String(c.records_scope_delta)} asks=${String(c.asks)}\n`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return c.verdict === 'unknown' ? 1 : 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _selfTest(): number {
|
|
182
|
+
const cases: ReadonlyArray<readonly [string, Verdict]> = [
|
|
183
|
+
['a missing test on a path this change touched', 'agent'],
|
|
184
|
+
['a boy-scout cleanup, small and local', 'agent'],
|
|
185
|
+
['two equal technical strategies with no evidence between them', 'council'],
|
|
186
|
+
['this introduces a compatibility risk', 'council'],
|
|
187
|
+
['it needs a typed op and no grant exists', 'owner'],
|
|
188
|
+
['this adds new user-visible semantics', 'owner'],
|
|
189
|
+
['an unrelated refactor that deserves its own roadmap', 'follow-up'],
|
|
190
|
+
['something nobody wrote a rule for', 'unknown'],
|
|
191
|
+
// The ordering case: matches `missing test` AND `unrelated`, and the
|
|
192
|
+
// conservative rung must win or a follow-up silently becomes agent work.
|
|
193
|
+
['a missing test in an unrelated subsystem', 'follow-up'],
|
|
194
|
+
];
|
|
195
|
+
const failures: string[] = [];
|
|
196
|
+
for (const [text, want] of cases) {
|
|
197
|
+
const got = classifyGrowth(text).verdict;
|
|
198
|
+
if (got !== want) failures.push(`"${text}" → ${got}, wanted ${want}`);
|
|
199
|
+
}
|
|
200
|
+
if (failures.length) {
|
|
201
|
+
for (const f of failures) process.stderr.write(`❌ ${_PROG} --self-test: ${f}\n`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(`✅ ${_PROG} --self-test: ${String(cases.length)} case(s) behaved\n`);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _isCliEntry(): boolean {
|
|
209
|
+
if (!process.argv[1]) return false;
|
|
210
|
+
try {
|
|
211
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
218
|
+
process.exitCode = main();
|
|
219
|
+
}
|
|
@@ -24,13 +24,22 @@
|
|
|
24
24
|
* Usage:
|
|
25
25
|
* sync_agent_settings # write (default)
|
|
26
26
|
* sync_agent_settings --dry-run # show diff, no write
|
|
27
|
-
* sync_agent_settings --check # exit 2
|
|
27
|
+
* sync_agent_settings --check # exit 2 if a sync is needed (CI)
|
|
28
28
|
* sync_agent_settings --profile balanced # use a specific profile
|
|
29
29
|
* sync_agent_settings --path path/to/.agent-settings.yml
|
|
30
30
|
*
|
|
31
31
|
* Exit codes:
|
|
32
32
|
* 0 — already in sync, or changes applied (or --dry-run ran cleanly)
|
|
33
|
-
* 2 —
|
|
33
|
+
* 2 — under --check, synchronization requires intervention; also invalid
|
|
34
|
+
* arguments, missing files, and a duplicate-key repair this tool
|
|
35
|
+
* refuses to perform.
|
|
36
|
+
*
|
|
37
|
+
* `--check` has two diagnostic cases and its stderr line already separates
|
|
38
|
+
* them, because the remedies differ: template drift ("drift detected"), and a
|
|
39
|
+
* duplicate-key repair with no drift ("duplicate keys need collapsing"). Both
|
|
40
|
+
* exit 2 — the target needs a write either way, and a green `--check` over a
|
|
41
|
+
* file the strict reader cannot parse would hide the breakage. A caller that
|
|
42
|
+
* needs the distinction reads the message, not the code.
|
|
34
43
|
*
|
|
35
44
|
* No behaviour changes vs. the retired Python implementation — historical quirks preserved (consumers pin the exact behaviour).
|
|
36
45
|
*/
|
|
@@ -441,6 +450,52 @@ function loadTemplate(p: string, profileValues: Record<string, string>): string
|
|
|
441
450
|
return renderTemplate(fs.readFileSync(p, 'utf-8'), profileValues);
|
|
442
451
|
}
|
|
443
452
|
|
|
453
|
+
/**
|
|
454
|
+
* The comment `mergeIntoTemplate` writes above its appended flat-key block.
|
|
455
|
+
*
|
|
456
|
+
* Duplicated here rather than imported: the writer holds it as a literal in
|
|
457
|
+
* `src/server/io/yamlIO.ts` and exports no constant, and that module belongs
|
|
458
|
+
* to the server tree this CLI does not otherwise depend on. Keep the two in
|
|
459
|
+
* step — a drift makes the sweep below silently stop matching.
|
|
460
|
+
*/
|
|
461
|
+
const WIZARD_BLOCK_HEADER = '# Wizard-added keys (no template entry)';
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Drop a wizard-block header the collapse emptied.
|
|
465
|
+
*
|
|
466
|
+
* The non-idempotent writer appended the header AND its keys on every save, so
|
|
467
|
+
* a collapse that removes the later block's key lines leaves that block's
|
|
468
|
+
* header behind with nothing under it — and the next append puts a fresh
|
|
469
|
+
* header below the orphan, one more per save. A header names the keys beneath
|
|
470
|
+
* it; with none beneath it, it names nothing and misleads the next reader.
|
|
471
|
+
*
|
|
472
|
+
* A header counts as emptied only when no key line stands between it and the
|
|
473
|
+
* next header or EOF. Blanks and further comments do not count: a user comment
|
|
474
|
+
* written between the header and its first key is part of the block, not a
|
|
475
|
+
* replacement for it.
|
|
476
|
+
*/
|
|
477
|
+
function dropOrphanedWizardHeaders(lines: readonly string[]): string[] {
|
|
478
|
+
const drop = new Set<number>();
|
|
479
|
+
for (let i = 0; i < lines.length; i++) {
|
|
480
|
+
if (lines[i]?.trim() !== WIZARD_BLOCK_HEADER) continue;
|
|
481
|
+
let hasKey = false;
|
|
482
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
483
|
+
const l = lines[j];
|
|
484
|
+
if (l === undefined) continue;
|
|
485
|
+
if (l.trim() === WIZARD_BLOCK_HEADER) break;
|
|
486
|
+
if (l.trim() === '' || l.trimStart().startsWith('#')) continue;
|
|
487
|
+
hasKey = true;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
if (hasKey) continue;
|
|
491
|
+
drop.add(i);
|
|
492
|
+
// The writer emits the blank line above the header as part of the block,
|
|
493
|
+
// so removing the header alone widens the gap by one line per repair.
|
|
494
|
+
if (i > 0 && lines[i - 1]?.trim() === '') drop.add(i - 1);
|
|
495
|
+
}
|
|
496
|
+
return drop.size === 0 ? [...lines] : lines.filter((_, i) => !drop.has(i));
|
|
497
|
+
}
|
|
498
|
+
|
|
444
499
|
export interface DuplicateRepair {
|
|
445
500
|
/** The document with every safely-collapsible duplicate run reduced to one. */
|
|
446
501
|
text: string;
|
|
@@ -482,6 +537,9 @@ export interface DuplicateRepair {
|
|
|
482
537
|
* A multi-document stream (`---` / `...`) is not repaired either. The reader
|
|
483
538
|
* takes a single document and rejects such a file whatever this pass does, so
|
|
484
539
|
* collapsing across a boundary could only merge two documents' keys into one.
|
|
540
|
+
*
|
|
541
|
+
* When — and only when — something did collapse, a wizard-block header the
|
|
542
|
+
* collapse emptied is removed with it; see `dropOrphanedWizardHeaders`.
|
|
485
543
|
*/
|
|
486
544
|
export function collapseDuplicateFlatKeys(text: string): DuplicateRepair {
|
|
487
545
|
// Line endings are preserved: splitting on `\n` alone leaves a trailing
|
|
@@ -630,10 +688,53 @@ export function collapseDuplicateFlatKeys(text: string): DuplicateRepair {
|
|
|
630
688
|
if (collapsed.length === 0) {
|
|
631
689
|
return { text, collapsed, unsafe };
|
|
632
690
|
}
|
|
633
|
-
const kept = lines.filter((_, i) => !drop.has(i));
|
|
691
|
+
const kept = dropOrphanedWizardHeaders(lines.filter((_, i) => !drop.has(i)));
|
|
634
692
|
return { text: kept.join(eol), collapsed, unsafe };
|
|
635
693
|
}
|
|
636
694
|
|
|
695
|
+
/**
|
|
696
|
+
* The parse error an input still carries once its duplicate keys are excused,
|
|
697
|
+
* or null when the duplicates were its only problem.
|
|
698
|
+
*
|
|
699
|
+
* `uniqueKeys: false` is what excuses them: the duplicate-key rejection is the
|
|
700
|
+
* single error the collapse has a mandate to remove, so anything the parser
|
|
701
|
+
* still raises is an error the collapse was never licensed to touch.
|
|
702
|
+
*/
|
|
703
|
+
function residualParseError(text: string): Error | null {
|
|
704
|
+
try {
|
|
705
|
+
parseYaml(text, { version: '1.1', uniqueKeys: false });
|
|
706
|
+
return null;
|
|
707
|
+
} catch (err) {
|
|
708
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* What the parser reliably exposes about a failure, as one indented block.
|
|
714
|
+
*
|
|
715
|
+
* Available on every `YAMLParseError` observed from this parser: `message`,
|
|
716
|
+
* whose first line already carries `at line L, column C` plus the detail, and
|
|
717
|
+
* `code`, a stable identifier. `linePos` is populated too, but only while
|
|
718
|
+
* `prettyErrors` stays on (the library's own condition), so it is printed when
|
|
719
|
+
* present and never relied on. Nothing here is asserted beyond the parser's
|
|
720
|
+
* own first message line, which is derived in the test rather than hardcoded —
|
|
721
|
+
* pinning a format this module does not own would make a safety test fail on a
|
|
722
|
+
* dependency's wording change.
|
|
723
|
+
*/
|
|
724
|
+
function formatParserDetail(err: Error): string {
|
|
725
|
+
const first = err.message.split('\n')[0] ?? err.message;
|
|
726
|
+
const parts: string[] = [` ${first}`];
|
|
727
|
+
const code = (err as { code?: unknown }).code;
|
|
728
|
+
const linePos = (err as { linePos?: { line: number; col: number }[] }).linePos;
|
|
729
|
+
const at = linePos?.[0];
|
|
730
|
+
if (typeof code === 'string' && at !== undefined) {
|
|
731
|
+
parts.push(` (${code} at line ${at.line}, column ${at.col})`);
|
|
732
|
+
} else if (typeof code === 'string') {
|
|
733
|
+
parts.push(` (${code})`);
|
|
734
|
+
}
|
|
735
|
+
return parts.join('\n') + '\n';
|
|
736
|
+
}
|
|
737
|
+
|
|
637
738
|
function loadUserText(raw: string): Record<string, unknown> {
|
|
638
739
|
const data = parseYaml(raw, { version: '1.1' });
|
|
639
740
|
if (data === null || data === undefined) {
|
|
@@ -863,6 +964,42 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
863
964
|
return 2;
|
|
864
965
|
}
|
|
865
966
|
if (repair.collapsed.length > 0) {
|
|
967
|
+
// Refuse before anything else in this branch, including the notice below:
|
|
968
|
+
// in write mode that notice is past tense ("collapsed …"), so announcing
|
|
969
|
+
// it and then refusing describes a write that never happened.
|
|
970
|
+
//
|
|
971
|
+
// The collapse's safety rests on preserving the original's last-wins
|
|
972
|
+
// reading, and a document that is invalid for some FURTHER reason has no
|
|
973
|
+
// reading to preserve — the pass can then emit valid YAML carrying
|
|
974
|
+
// structure nobody authored, which every downstream check accepts. A
|
|
975
|
+
// stderr warning (what this branch used to do) is a weak control against a
|
|
976
|
+
// silent outcome; a refusal is loud and the operator can still recover.
|
|
977
|
+
// AI council, 2026-09-11, 2 of 2 seats, converged — including a rejection
|
|
978
|
+
// of any `--repair-anyway` escape: an operator who understands the file
|
|
979
|
+
// well enough to use it understands it well enough to fix the error first,
|
|
980
|
+
// and "read the diff" is not a trustworthy oracle for YAML semantics.
|
|
981
|
+
//
|
|
982
|
+
// Exit 2, per the taxonomy in this file's header: 2 is "this tool cannot
|
|
983
|
+
// proceed with this input", which is also what the sibling refusal above
|
|
984
|
+
// (duplicates it will not collapse) returns. 1 is spoken for — it is the
|
|
985
|
+
// mirrored `install.fail` exit and means a broken install asset.
|
|
986
|
+
//
|
|
987
|
+
// Nothing durable precedes this point: the only write in `main` is the
|
|
988
|
+
// single `writeFileSync` at the end, there is no temporary file and no
|
|
989
|
+
// rename, so the refusal leaves the target byte-for-byte unchanged.
|
|
990
|
+
const residual = residualParseError(rawText);
|
|
991
|
+
if (residual !== null) {
|
|
992
|
+
process.stderr.write(
|
|
993
|
+
`❌ ${target}: refusing the automatic duplicate repair — the file has ` +
|
|
994
|
+
`another YAML error besides its duplicate keys.\n` +
|
|
995
|
+
formatParserDetail(residual) +
|
|
996
|
+
` Collapsing changes only WHETHER a file parses, never what it parses as, ` +
|
|
997
|
+
`and that promise is empty over a document with no valid reading.\n` +
|
|
998
|
+
` Fix the syntax error by hand, then re-run to apply the duplicate repair.\n`,
|
|
999
|
+
);
|
|
1000
|
+
return 2;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
866
1003
|
// NOT gated on --quiet, and not on stdout. Both callers that matter run
|
|
867
1004
|
// quiet — the release's `sync-agent-settings` step and the pre-release
|
|
868
1005
|
// probe — so gating this would let a release rewrite the operator's own
|
|
@@ -878,21 +1015,6 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
878
1015
|
? `collapsed ${repair.collapsed.length} duplicate key(s), last value kept`
|
|
879
1016
|
: `would collapse ${repair.collapsed.length} duplicate key(s), keeping the last value (no write in this mode)`;
|
|
880
1017
|
process.stderr.write(`🔧 ${target}: ${verb} — ${repair.collapsed.join(', ')}\n`);
|
|
881
|
-
|
|
882
|
-
// The collapse promises to change only WHETHER the file parses, never what
|
|
883
|
-
// it parses as — and that promise is only meaningful over an input that had
|
|
884
|
-
// a reading to preserve. If the original was unparseable for some further
|
|
885
|
-
// reason (an unterminated quote, say), the repaired file may parse to
|
|
886
|
-
// structure no human wrote, and the next `git diff` shows keys nobody
|
|
887
|
-
// added. Cheap to say, and the operator cannot recover it from the output.
|
|
888
|
-
try {
|
|
889
|
-
parseYaml(rawText, { version: '1.1', uniqueKeys: false });
|
|
890
|
-
} catch {
|
|
891
|
-
process.stderr.write(
|
|
892
|
-
`⚠️ ${target}: the original had a syntax error beyond the duplicate keys, so the ` +
|
|
893
|
-
`result may differ in structure, not only in duplicates — read the diff before trusting it.\n`,
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
1018
|
}
|
|
897
1019
|
const sourceText = repair.text;
|
|
898
1020
|
|