@event4u/agent-config 9.25.0 → 9.27.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 +58 -143
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +21 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +256 -0
- package/dist/agent-src/commands/roadmap.md +12 -4
- package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
- package/dist/agent-src/rules/architecture.md +2 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
- package/dist/agent-src/rules/artifact-engagement-recording.md +2 -0
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +2 -0
- package/dist/agent-src/rules/brand-consistency.md +2 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +2 -0
- package/dist/agent-src/rules/broken-access-control.md +2 -0
- package/dist/agent-src/rules/cli-output-handling.md +2 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/code-provenance.md +2 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +2 -0
- package/dist/agent-src/rules/content-quoting-floor.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +10 -0
- package/dist/agent-src/rules/copilot-routing.md +2 -0
- package/dist/agent-src/rules/cross-source-consistency.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -2
- package/dist/agent-src/rules/design-fidelity.md +19 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +120 -0
- package/dist/agent-src/rules/devcontainer-routing.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +2 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
- package/dist/agent-src/rules/domain-safety-pii.md +2 -0
- package/dist/agent-src/rules/domain-safety-retention.md +2 -0
- package/dist/agent-src/rules/downstream-changes.md +2 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/evaluator-independence.md +19 -3
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
- package/dist/agent-src/rules/finance-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +3 -1
- package/dist/agent-src/rules/guidelines.md +2 -0
- package/dist/agent-src/rules/history-discipline.md +2 -0
- package/dist/agent-src/rules/icon-consistency.md +2 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
- package/dist/agent-src/rules/improve-before-implement.md +2 -0
- package/dist/agent-src/rules/invite-challenge.md +2 -0
- package/dist/agent-src/rules/laravel-routing.md +2 -0
- package/dist/agent-src/rules/laravel-translations.md +2 -0
- package/dist/agent-src/rules/legal-safety-floor.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/missing-tool-handling.md +2 -0
- package/dist/agent-src/rules/model-recommendation.md +2 -0
- package/dist/agent-src/rules/no-attribution-footers.md +2 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/package-ci-checks.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +2 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
- package/dist/agent-src/rules/question-not-instruction.md +2 -0
- package/dist/agent-src/rules/reviewer-awareness.md +2 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/role-mode-adherence.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/runtime-safety.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
- package/dist/agent-src/rules/session-canary.md +63 -19
- package/dist/agent-src/rules/settings-ask-protocol.md +180 -0
- package/dist/agent-src/rules/size-enforcement.md +2 -0
- package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-discovery-gate.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/symfony-routing.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +2 -0
- package/dist/agent-src/rules/token-budget-discipline.md +8 -3
- package/dist/agent-src/rules/token-efficiency.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +2 -0
- package/dist/agent-src/rules/ui-audit-gate.md +7 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/upstream-proposal.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +5 -2
- package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-3KJIMFTB.js} +1 -1
- package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-BCPIOAGV.js} +148 -133
- package/dist/cli-delegate/cmd_conformance.js +1 -1
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +191 -120
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/hooks/dispatch.js +1148 -349
- package/dist/install/install.mjs +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +571 -417
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +33 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/io/yamlIO.js +85 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/nicknameResolver.js +116 -0
- package/dist/server/nicknameResolver.js.map +1 -0
- package/dist/server/routes/ping.js +29 -11
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/settings.js +1 -12
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/wizard.js +52 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/interactiveContext.js +73 -0
- package/dist/shared/interactiveContext.js.map +1 -0
- package/dist/shared/nicknamePrefill.js +59 -0
- package/dist/shared/nicknamePrefill.js.map +1 -0
- package/dist/shared/settingsAsks.js +128 -0
- package/dist/shared/settingsAsks.js.map +1 -0
- package/dist/shared/settingsCarveOut.js +118 -0
- package/dist/shared/settingsCarveOut.js.map +1 -0
- package/dist/shared/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/dist/shared/settingsSurface.js +7 -1
- package/dist/shared/settingsSurface.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -1
- package/src/config/gate-violation-baselines.json +13 -7
- package/src/scripts/_cli/cmd_doctor.ts +6 -21
- package/src/scripts/_cli/cmd_route_audit.ts +4 -1
- package/src/scripts/_cli/cmd_route_explain.ts +9 -1
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/git_common_dir.ts +128 -0
- package/src/scripts/_lib/obligation_frequency.ts +391 -0
- package/src/scripts/_lib/session_register.ts +361 -0
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +23 -4
- package/src/scripts/bench_ab_v2_run.ts +272 -16
- package/src/scripts/bench_ab_v2_stats.ts +156 -3
- package/src/scripts/check_completion_review.ts +80 -3
- package/src/scripts/check_corpus_staleness.ts +692 -0
- package/src/scripts/check_enforcement_coverage.ts +192 -7
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/check_rule_projection_integrity.ts +364 -0
- package/src/scripts/condense.ts +36 -2
- package/src/scripts/conformance_scan.ts +82 -17
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/generate_settings_reference.ts +232 -0
- package/src/scripts/git_authorization_hook.ts +75 -10
- package/src/scripts/hook_manifest.yaml +87 -27
- package/src/scripts/hooks/block_no_verify.ts +59 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +199 -16
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +13 -1
- package/src/scripts/hooks/dispatch_hook.ts +97 -2
- package/src/scripts/hooks/evidence_independence.ts +40 -6
- package/src/scripts/language_mirror_hook.ts +256 -17
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/orchestration_backfill.ts +354 -0
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +180 -37
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/session_canary_hook.ts +35 -3
- package/src/scripts/session_register_hook.ts +288 -0
- package/src/scripts/sessions_cli.ts +140 -0
- package/src/scripts/validate_frontmatter.ts +68 -0
- package/src/server/io/yamlIO.ts +87 -0
- package/src/shared/interactiveContext.ts +98 -0
- package/src/shared/nicknamePrefill.ts +96 -0
- package/src/shared/settingsAsks.ts +176 -0
- package/src/shared/settingsCarveOut.ts +143 -0
- package/src/shared/settingsConsent.ts +136 -0
- package/src/shared/settingsSurface.ts +7 -1
|
@@ -32,6 +32,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
32
32
|
import { parse as parseYaml } from 'yaml';
|
|
33
33
|
|
|
34
34
|
import { artefact_roots } from './_lib/agent_src.js';
|
|
35
|
+
import { KERNEL_RULE_ID_SET } from './_lib/kernel_rules.js';
|
|
36
|
+
import {
|
|
37
|
+
frequency_prose_conflicts,
|
|
38
|
+
has_frequency_override,
|
|
39
|
+
is_frequency,
|
|
40
|
+
} from './_lib/obligation_frequency.js';
|
|
35
41
|
|
|
36
42
|
// Free-form YAML value alias. The lenient subset parser produces strings,
|
|
37
43
|
// numbers, booleans, the empty string, nested objects, and arrays of those.
|
|
@@ -807,6 +813,65 @@ function pyListRepr(values: YamlValue[]): string {
|
|
|
807
813
|
return `[${values.map((v) => pyRepr(v)).join(', ')}]`;
|
|
808
814
|
}
|
|
809
815
|
|
|
816
|
+
/**
|
|
817
|
+
* Rule-only checks for `obligation_frequency` — the field the coverage join
|
|
818
|
+
* reads.
|
|
819
|
+
*
|
|
820
|
+
* Two obligations, at two severities:
|
|
821
|
+
*
|
|
822
|
+
* - **Required, as an error.** A rule with no declared period cannot be joined
|
|
823
|
+
* against its carrier's firing frequency, so the audit reports it
|
|
824
|
+
* `unclassified` — which is a hole in the finding, not a pass. Making it
|
|
825
|
+
* mandatory at authoring time is the pin: a NEW rule with a per-turn
|
|
826
|
+
* obligation and no turn-carrier is caught here rather than in a later audit.
|
|
827
|
+
*
|
|
828
|
+
* - **Prose agreement, as a warning.** A keyword heuristic will be noisy, so it
|
|
829
|
+
* never fails a build.
|
|
830
|
+
*
|
|
831
|
+
* The nine kernel rules are exempt, and the exemption is DERIVED from
|
|
832
|
+
* `_lib/kernel_rules.ts` — the same locked set `block_kernel_rule_writes.ts`
|
|
833
|
+
* enforces. They are not exempt because kernel rules are special; they are
|
|
834
|
+
* exempt because that guard denies the write, so requiring the field would make
|
|
835
|
+
* `task ci` unsatisfiable for any agent. Deriving it rather than hand-listing it
|
|
836
|
+
* closes the exemption automatically: the moment a rule leaves the kernel, the
|
|
837
|
+
* field becomes required with no edit here.
|
|
838
|
+
*/
|
|
839
|
+
export function check_obligation_frequency(
|
|
840
|
+
filePath: string,
|
|
841
|
+
text: string,
|
|
842
|
+
data: Record<string, YamlValue>,
|
|
843
|
+
): SchemaError[] {
|
|
844
|
+
const id = path.basename(filePath).replace(/\.md$/, '');
|
|
845
|
+
if (KERNEL_RULE_ID_SET.has(id)) return [];
|
|
846
|
+
|
|
847
|
+
const declared = data['obligation_frequency'];
|
|
848
|
+
if (!is_frequency(declared)) {
|
|
849
|
+
return [
|
|
850
|
+
new SchemaError(
|
|
851
|
+
'$.obligation_frequency',
|
|
852
|
+
'required',
|
|
853
|
+
"Missing required property 'obligation_frequency' — how often this rule's " +
|
|
854
|
+
'obligation comes due. Without it check_enforcement_coverage cannot join ' +
|
|
855
|
+
"the rule against its carrier's firing frequency and reports it unclassified.",
|
|
856
|
+
),
|
|
857
|
+
];
|
|
858
|
+
}
|
|
859
|
+
if (has_frequency_override(text)) return [];
|
|
860
|
+
|
|
861
|
+
const conflicts = frequency_prose_conflicts(text, declared);
|
|
862
|
+
if (conflicts.length === 0) return [];
|
|
863
|
+
return [
|
|
864
|
+
new SchemaError(
|
|
865
|
+
'$.obligation_frequency',
|
|
866
|
+
'frequency-prose-drift',
|
|
867
|
+
`declared '${declared}', but the prose reads as ${conflicts.join(' / ')} — ` +
|
|
868
|
+
'either the declaration or the prose moved. Silence an audited mismatch with a ' +
|
|
869
|
+
'`# frequency-override: <reason>` comment in the frontmatter.',
|
|
870
|
+
'warning',
|
|
871
|
+
),
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
|
|
810
875
|
// --- CLI entry point -------------------------------------------------------
|
|
811
876
|
|
|
812
877
|
/** Yield `[artefact_type, path]` pairs for all lintable artefacts. */
|
|
@@ -931,6 +996,9 @@ export function _main(argv: string[]): number {
|
|
|
931
996
|
// (road-to-abstraction-reduction.md Phase 1).
|
|
932
997
|
apply_schema_defaults(data, schema);
|
|
933
998
|
const errors = validate(data, schema);
|
|
999
|
+
if (artefactType === 'rule') {
|
|
1000
|
+
errors.push(...check_obligation_frequency(p, text, data));
|
|
1001
|
+
}
|
|
934
1002
|
const fatal = errors.filter((e) => e.severity === 'error');
|
|
935
1003
|
const warnings = errors.filter((e) => e.severity === 'warning');
|
|
936
1004
|
if (fatal.length > 0) {
|
package/src/server/io/yamlIO.ts
CHANGED
|
@@ -130,6 +130,93 @@ export function mergeIntoTemplate(templateBody: string, newValues: Record<string
|
|
|
130
130
|
return body;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Placeholders the shipped template carries, and the value each resolves to
|
|
135
|
+
* when nothing has substituted it. The installer fills these from the chosen
|
|
136
|
+
* profile preset; anything reading the template directly must substitute them
|
|
137
|
+
* too, or it writes a literal `__PLACEHOLDER__` into the user's file.
|
|
138
|
+
*/
|
|
139
|
+
export const TEMPLATE_PLACEHOLDER_DEFAULTS: Readonly<Record<string, string>> = {
|
|
140
|
+
__RULE_LOADING_TIER__: 'balanced',
|
|
141
|
+
// Successor knob (ADR-110). P2-verdict council 2026-07-07: the
|
|
142
|
+
// balanced-heritage default is `auto` — lift only where measured
|
|
143
|
+
// (vendor-granular unknown_defaults in src/config/host-capabilities.yml).
|
|
144
|
+
__DISCIPLINE_PROFILE__: 'auto',
|
|
145
|
+
__USER_TYPE__: '',
|
|
146
|
+
__CHAT_HISTORY_FREQUENCY__: 'per_turn',
|
|
147
|
+
__CHAT_HISTORY_MAX_SIZE_KB__: '2048',
|
|
148
|
+
__CHAT_HISTORY_ON_OVERFLOW__: 'rotate',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Replace every known template placeholder with its default value. */
|
|
152
|
+
export function substituteTemplatePlaceholders(body: string): string {
|
|
153
|
+
let rendered = body;
|
|
154
|
+
for (const [placeholder, value] of Object.entries(TEMPLATE_PLACEHOLDER_DEFAULTS)) {
|
|
155
|
+
rendered = rendered.replaceAll(placeholder, value);
|
|
156
|
+
}
|
|
157
|
+
return rendered;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Read one dotted path out of a parsed settings tree. `undefined` when absent. */
|
|
161
|
+
export function readPath(root: Record<string, unknown>, dotted: string): unknown {
|
|
162
|
+
let node: unknown = root;
|
|
163
|
+
for (const segment of dotted.split('.')) {
|
|
164
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) return undefined;
|
|
165
|
+
node = (node as Record<string, unknown>)[segment];
|
|
166
|
+
}
|
|
167
|
+
return node;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Write one dotted path into a settings tree, creating intermediate objects. */
|
|
171
|
+
export function writePath(root: Record<string, unknown>, dotted: string, value: unknown): void {
|
|
172
|
+
const segments = dotted.split('.');
|
|
173
|
+
const leaf = segments.pop();
|
|
174
|
+
if (leaf === undefined) return;
|
|
175
|
+
let node = root;
|
|
176
|
+
for (const segment of segments) {
|
|
177
|
+
const next = node[segment];
|
|
178
|
+
if (next === null || typeof next !== 'object' || Array.isArray(next)) {
|
|
179
|
+
const created: Record<string, unknown> = {};
|
|
180
|
+
node[segment] = created;
|
|
181
|
+
node = created;
|
|
182
|
+
} else {
|
|
183
|
+
node = next as Record<string, unknown>;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
node[leaf] = value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Render a SPARSE settings document — the decisions actually made, and nothing
|
|
191
|
+
* else (`road-to-zero-ceremony-settings` Phase 3).
|
|
192
|
+
*
|
|
193
|
+
* The counterpart to `mergeIntoTemplate`, which returns the whole 1,233-line
|
|
194
|
+
* template with the answers patched in. Here the template is consulted only as
|
|
195
|
+
* the value source for the carve-out keys; it is never copied into the output.
|
|
196
|
+
*
|
|
197
|
+
* Comment preservation is not a concern in this direction, because there are no
|
|
198
|
+
* template comments to preserve — the long-form explanation those comments used
|
|
199
|
+
* to carry now lives in the generated `docs/settings-reference.md`, which the
|
|
200
|
+
* header points at.
|
|
201
|
+
*/
|
|
202
|
+
export function renderSparseSettings(values: Record<string, unknown>): string {
|
|
203
|
+
const header = [
|
|
204
|
+
'# Your settings — a record of decisions, not a copy of the defaults.',
|
|
205
|
+
'#',
|
|
206
|
+
'# Every key absent from this file resolves to its documented default.',
|
|
207
|
+
'# The full key list, with defaults and explanations, is generated at',
|
|
208
|
+
'# docs/settings-reference.md — this file stays small on purpose.',
|
|
209
|
+
'#',
|
|
210
|
+
'# How each entry was decided is recorded beside it in',
|
|
211
|
+
'# `.agent-settings.provenance.json`.',
|
|
212
|
+
'',
|
|
213
|
+
].join('\n');
|
|
214
|
+
const body = Object.keys(values).length === 0
|
|
215
|
+
? ''
|
|
216
|
+
: yamlDump(values, { lineWidth: -1, sortKeys: true });
|
|
217
|
+
return `${header}${body}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
133
220
|
export function diffValues(
|
|
134
221
|
before: Record<string, unknown>,
|
|
135
222
|
after: Record<string, unknown>,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is there a human at the other end of this execution?
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of `road-to-zero-ceremony-settings`: *"Skip cleanly in non-TTY, CI,
|
|
5
|
+
* and headless contexts: no file, defaults, no questions, ever."* A settings
|
|
6
|
+
* question posed where nobody can answer it is not a question — it is either a
|
|
7
|
+
* hang or a silently-taken default wearing a prompt's clothes.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS MODULE EXISTS RATHER THAN A FOURTH INLINE CHECK. The tree already
|
|
10
|
+
* carries several CI-detection shapes that disagree:
|
|
11
|
+
*
|
|
12
|
+
* - `set && !== '0'` — `src/cli/firstRunNotice.ts`, `src/cli/initRouting.ts`
|
|
13
|
+
* - truthy only — `src/scripts/install.ts`, `src/scripts/release.ts`
|
|
14
|
+
* - exact match — `src/scripts/_lib/exec_evidence.ts` (`'true' | '1'`)
|
|
15
|
+
* - widest, three variables — `src/scripts/reach_doctor.ts`
|
|
16
|
+
* (`CI`, `GITHUB_ACTIONS`, `AGENT_CONFIG_CI`)
|
|
17
|
+
*
|
|
18
|
+
* This module takes the **widest** shape, because the failure modes are
|
|
19
|
+
* asymmetric: a false "interactive" hangs an automated run or fabricates a
|
|
20
|
+
* consent, while a false "non-interactive" merely declines to ask and takes a
|
|
21
|
+
* conservative default — which is the documented behaviour anyway.
|
|
22
|
+
*
|
|
23
|
+
* It deliberately does NOT rewrite those four call sites. Their behaviour is
|
|
24
|
+
* correct for what they gate (a printed notice, a browser launch), and
|
|
25
|
+
* converting them here would be a drive-by change to code this concern does not
|
|
26
|
+
* own. The divergence is recorded rather than repaired — see the roadmap note on
|
|
27
|
+
* Phase 4 step 3.
|
|
28
|
+
*
|
|
29
|
+
* Pure — the environment and the TTY flags are arguments, never read from
|
|
30
|
+
* `process` inside the predicate, so a test never depends on the machine it
|
|
31
|
+
* runs on.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Why an execution cannot host a question. `null` means it can. */
|
|
35
|
+
export type NonInteractiveReason =
|
|
36
|
+
| 'ci'
|
|
37
|
+
| 'no-ui-requested'
|
|
38
|
+
| 'not-a-tty'
|
|
39
|
+
| 'headless';
|
|
40
|
+
|
|
41
|
+
export interface SessionProbe {
|
|
42
|
+
/** `process.env`, or a fixture. */
|
|
43
|
+
env: NodeJS.ProcessEnv;
|
|
44
|
+
/** `process.stdin.isTTY === true`. */
|
|
45
|
+
stdinTty: boolean;
|
|
46
|
+
/** `process.stdout.isTTY === true`. */
|
|
47
|
+
stdoutTty: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* A display-less environment (SSH without forwarding, Linux without
|
|
50
|
+
* `DISPLAY`). Supplied by the caller because probing it is platform work,
|
|
51
|
+
* and `src/cli/commands/uiServe.ts` already owns that probe.
|
|
52
|
+
*/
|
|
53
|
+
headless?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The CI variables this predicate honours, widest-first.
|
|
58
|
+
*
|
|
59
|
+
* A variable counts as set when it is present and not the literal `0` — the
|
|
60
|
+
* `set && !== '0'` convention already used by the first-run notice, kept so a
|
|
61
|
+
* consumer who exports `CI=0` to mean "not CI" is believed.
|
|
62
|
+
*/
|
|
63
|
+
const CI_ENV_KEYS = ['CI', 'GITHUB_ACTIONS', 'AGENT_CONFIG_CI'] as const;
|
|
64
|
+
|
|
65
|
+
function _flagSet(env: NodeJS.ProcessEnv, key: string): boolean {
|
|
66
|
+
const raw = (env[key] ?? '').trim();
|
|
67
|
+
return raw !== '' && raw !== '0';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The reason this execution may not ask, or `null` when a human is reachable.
|
|
72
|
+
*
|
|
73
|
+
* Order is deliberate and is the order a reader should think in: an explicit
|
|
74
|
+
* request not to be asked outranks a capability probe, and CI outranks
|
|
75
|
+
* everything because a CI run that asks is a CI run that hangs.
|
|
76
|
+
*/
|
|
77
|
+
export function nonInteractiveReason(probe: SessionProbe): NonInteractiveReason | null {
|
|
78
|
+
for (const key of CI_ENV_KEYS) {
|
|
79
|
+
if (_flagSet(probe.env, key)) {
|
|
80
|
+
return 'ci';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (_flagSet(probe.env, 'AGENT_CONFIG_NO_UI')) {
|
|
84
|
+
return 'no-ui-requested';
|
|
85
|
+
}
|
|
86
|
+
if (!probe.stdinTty || !probe.stdoutTty) {
|
|
87
|
+
return 'not-a-tty';
|
|
88
|
+
}
|
|
89
|
+
if (probe.headless === true) {
|
|
90
|
+
return 'headless';
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Convenience inverse — `true` when a question is answerable. */
|
|
96
|
+
export function isInteractiveSession(probe: SessionProbe): boolean {
|
|
97
|
+
return nonInteractiveReason(probe) === null;
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to put in the nickname field before the user types anything.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 step 2 of `road-to-zero-ceremony-settings`: *"Ask the nickname once,
|
|
5
|
+
* prefilled from git user name then `$USER`, so accepting is one keypress."*
|
|
6
|
+
* `src/rules/settings-ask-protocol.md` states the chain and closes it with a
|
|
7
|
+
* negation — **"Never `$USER` alone."**
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS MODULE EXISTS. The rule shipped that chain; the one surface that
|
|
10
|
+
* actually prefills shipped a different one. `src/server/routes/ping.ts` read
|
|
11
|
+
* `userInfo().username` and nothing else — precisely the `$USER`-alone shape the
|
|
12
|
+
* rule forbids. A rule and the code it describes disagreeing is drift, not a
|
|
13
|
+
* preference, so the chain now has exactly one implementation and both the
|
|
14
|
+
* server route and the tests read it from here.
|
|
15
|
+
*
|
|
16
|
+
* WHY GIT COMES FIRST. `$USER` is a login handle (`mathiasberg`); `git config
|
|
17
|
+
* user.name` is the name the human chose to be called by (`Mathias Berg`). The
|
|
18
|
+
* whole point of the prefill is that accepting costs one keypress, and that only
|
|
19
|
+
* holds when the prefilled value is the one they would have typed. Note the
|
|
20
|
+
* lookup is deliberately NOT `--local`: the merged config includes
|
|
21
|
+
* `~/.gitconfig`, so the wizard still finds a name when it runs outside a repo.
|
|
22
|
+
*
|
|
23
|
+
* Pure: the git lookup is injected, so the resolver is a function of its probe
|
|
24
|
+
* alone and a test never depends on the machine it runs on — the same shape as
|
|
25
|
+
* `interactiveContext.ts` and `settingsConsent.ts`. The Node-side half (running
|
|
26
|
+
* git, falling back to `userInfo()`) lives in `src/server/nicknameResolver.ts`,
|
|
27
|
+
* because `src/shared/**` may not import Node built-ins and the lint enforces it.
|
|
28
|
+
*
|
|
29
|
+
* @see src/server/nicknameResolver.ts — the impure half
|
|
30
|
+
* @see src/rules/settings-ask-protocol.md § Worked example — the nickname
|
|
31
|
+
* @see docs/contracts/settings-classes.md
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where a candidate name came from, so a caller can say so.
|
|
36
|
+
*
|
|
37
|
+
* `os-account` is the `userInfo()` floor, and it is a distinct member rather
|
|
38
|
+
* than a reuse of `env-user` on purpose: the whole claim of the floor is that
|
|
39
|
+
* its RANK changed (last, not first), and a rank nothing can observe is not a
|
|
40
|
+
* rank. A caller reporting provenance would otherwise name the wrong rung on
|
|
41
|
+
* exactly the path the floor exists to serve.
|
|
42
|
+
*
|
|
43
|
+
* `none` is a real outcome, not an error: a machine with no git identity, no
|
|
44
|
+
* `USER` and no resolvable account is unusual but legal, and the ask still
|
|
45
|
+
* works — it just starts empty.
|
|
46
|
+
*/
|
|
47
|
+
export type NicknameSource =
|
|
48
|
+
| 'git-user-name'
|
|
49
|
+
| 'env-user'
|
|
50
|
+
| 'env-username'
|
|
51
|
+
| 'os-account'
|
|
52
|
+
| 'none';
|
|
53
|
+
|
|
54
|
+
export interface NicknamePrefill {
|
|
55
|
+
/** The prefill value, or `''` when nothing resolved. */
|
|
56
|
+
name: string;
|
|
57
|
+
source: NicknameSource;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NicknameProbe {
|
|
61
|
+
/** `process.env`, or a fixture. */
|
|
62
|
+
env: NodeJS.ProcessEnv;
|
|
63
|
+
/**
|
|
64
|
+
* `git config user.name`, or `undefined` when git is absent, unconfigured,
|
|
65
|
+
* or too slow. Injected so the resolver stays pure.
|
|
66
|
+
*/
|
|
67
|
+
gitUserName?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Trim, and treat a whitespace-only value as absent. */
|
|
71
|
+
function _clean(value: string | undefined): string {
|
|
72
|
+
return (value ?? '').trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The prefill for the nickname ask, following the documented chain.
|
|
77
|
+
*
|
|
78
|
+
* Order is the rule's, and the negation at the end of it is the load-bearing
|
|
79
|
+
* part: `$USER` is a fallback, never the first answer.
|
|
80
|
+
*/
|
|
81
|
+
export function nicknamePrefill(probe: NicknameProbe): NicknamePrefill {
|
|
82
|
+
const git = _clean(probe.gitUserName);
|
|
83
|
+
if (git !== '') {
|
|
84
|
+
return { name: git, source: 'git-user-name' };
|
|
85
|
+
}
|
|
86
|
+
const user = _clean(probe.env['USER']);
|
|
87
|
+
if (user !== '') {
|
|
88
|
+
return { name: user, source: 'env-user' };
|
|
89
|
+
}
|
|
90
|
+
// Windows spells it differently; the rule names it explicitly.
|
|
91
|
+
const username = _clean(probe.env['USERNAME']);
|
|
92
|
+
if (username !== '') {
|
|
93
|
+
return { name: username, source: 'env-username' };
|
|
94
|
+
}
|
|
95
|
+
return { name: '', source: 'none' };
|
|
96
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-question budget, as a function rather than as a promise.
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 of `road-to-zero-ceremony-settings`. The rule
|
|
5
|
+
* `src/rules/settings-ask-protocol.md` states the budget — at most ONE settings
|
|
6
|
+
* question per command execution, every further undecided key taking its
|
|
7
|
+
* conservative default silently and being named in the end-summary. Prose is
|
|
8
|
+
* where that obligation has to live, because no gate can count the questions in
|
|
9
|
+
* a chat turn.
|
|
10
|
+
*
|
|
11
|
+
* But the *split* is not a judgement call: given the keys an execution needs
|
|
12
|
+
* decided, which one is asked and what the others resolve to is fully
|
|
13
|
+
* determined. Leaving that determined part in prose too would mean the roadmap's
|
|
14
|
+
* own exit criterion — "a planted fixture needing three B decisions asks once,
|
|
15
|
+
* assumes two conservatively, and lists both in the summary" — had nothing to
|
|
16
|
+
* run against. So the split is computed here and the rule points at it.
|
|
17
|
+
*
|
|
18
|
+
* What this module does NOT do, stated so nobody mistakes it for the fence:
|
|
19
|
+
* it does not ask, it does not write, and it cannot stop an agent that ignores
|
|
20
|
+
* it. The write fence is one layer down and is real —
|
|
21
|
+
* `src/scripts/_cli/cmd_settings_set.ts` and the Fastify settings route both
|
|
22
|
+
* refuse every C-class key and fail closed on an unreadable class contract.
|
|
23
|
+
*
|
|
24
|
+
* Pure — no I/O, no clock, no environment.
|
|
25
|
+
*
|
|
26
|
+
* @see src/rules/settings-ask-protocol.md
|
|
27
|
+
* @see docs/contracts/settings-classes.md
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { classOfPath, isConservativeDefault, type SettingsClass } from './settingsClasses.js';
|
|
31
|
+
|
|
32
|
+
/** A key this execution resolved without asking, and how to change it later. */
|
|
33
|
+
export interface SilentDefault {
|
|
34
|
+
key: string;
|
|
35
|
+
/** The value taken — the template default, which for B is conservative. */
|
|
36
|
+
value: unknown;
|
|
37
|
+
/** The exact command that changes it, for the end-summary line. */
|
|
38
|
+
command: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A key that never reaches the ask path, and the reason it does not. */
|
|
42
|
+
export interface SkippedKey {
|
|
43
|
+
key: string;
|
|
44
|
+
reason:
|
|
45
|
+
| 'class-a-never-asked'
|
|
46
|
+
| 'class-c-guarded'
|
|
47
|
+
| 'unclassified'
|
|
48
|
+
| 'already-decided'
|
|
49
|
+
| 'non-conservative-default';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AskPlan {
|
|
53
|
+
/** The single key that gets this execution's question, or `null` for none. */
|
|
54
|
+
ask: string | null;
|
|
55
|
+
/** Undecided keys that took their default silently, in the order given. */
|
|
56
|
+
silent: SilentDefault[];
|
|
57
|
+
/** Keys excluded from the ask path entirely, with the reason. */
|
|
58
|
+
skipped: SkippedKey[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The end-summary line for a silently defaulted key.
|
|
63
|
+
*
|
|
64
|
+
* `--source manual` and not `jit-answer`: the user did not answer a question, so
|
|
65
|
+
* stamping the write as a just-in-time answer would record a consent nobody
|
|
66
|
+
* gave. The provenance vocabulary exists to keep exactly that distinction.
|
|
67
|
+
*/
|
|
68
|
+
export function changeCommand(key: string): string {
|
|
69
|
+
return `agent-config settings:set ${key} <value> --source manual`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Plan one execution's settings asks.
|
|
74
|
+
*
|
|
75
|
+
* `needed` is the keys this execution genuinely cannot proceed without, in the
|
|
76
|
+
* order they became needed — the caller's own ordering is respected because the
|
|
77
|
+
* first key to actually block the run is the one that earned the question.
|
|
78
|
+
*
|
|
79
|
+
* `decided` is every key already settled on any resolution layer. A decided key
|
|
80
|
+
* is not a candidate: re-asking a settled question is the cheap question the
|
|
81
|
+
* ask-discipline rules forbid, and it is the single most likely way this budget
|
|
82
|
+
* gets spent on nothing.
|
|
83
|
+
*
|
|
84
|
+
* `defaultOf` supplies the template default. It returns `undefined` for a key
|
|
85
|
+
* the template does not carry, which is treated as unclassified rather than as
|
|
86
|
+
* a default of `undefined` — a missing template entry is a defect, not a value.
|
|
87
|
+
*/
|
|
88
|
+
export function planSettingsAsks(
|
|
89
|
+
needed: readonly string[],
|
|
90
|
+
classes: ReadonlyMap<string, SettingsClass>,
|
|
91
|
+
defaultOf: (key: string) => unknown,
|
|
92
|
+
decided: ReadonlySet<string> = new Set(),
|
|
93
|
+
options: { interactive?: boolean } = {},
|
|
94
|
+
): AskPlan {
|
|
95
|
+
const plan: AskPlan = { ask: null, silent: [], skipped: [] };
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
// Non-TTY / CI / headless: nobody can answer, so the budget is ZERO rather
|
|
98
|
+
// than one. Every candidate takes its conservative default and the plan
|
|
99
|
+
// carries no ask — "no questions, ever", as a computed fact rather than as a
|
|
100
|
+
// promise the caller has to remember. Nothing is written either: this
|
|
101
|
+
// function never persists, and with no ask there is no `jit-answer` write.
|
|
102
|
+
const interactive = options.interactive ?? true;
|
|
103
|
+
|
|
104
|
+
for (const key of needed) {
|
|
105
|
+
// A duplicate in the caller's list must not consume the budget twice.
|
|
106
|
+
if (seen.has(key)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
seen.add(key);
|
|
110
|
+
|
|
111
|
+
if (decided.has(key)) {
|
|
112
|
+
plan.skipped.push({ key, reason: 'already-decided' });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cls = classOfPath(classes, key);
|
|
117
|
+
if (cls === undefined) {
|
|
118
|
+
// Unclassified is guarded everywhere else in this system; it is
|
|
119
|
+
// guarded here too. Never fall back to "probably fine".
|
|
120
|
+
plan.skipped.push({ key, reason: 'unclassified' });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (cls === 'A') {
|
|
124
|
+
plan.skipped.push({ key, reason: 'class-a-never-asked' });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (cls === 'C') {
|
|
128
|
+
// A C key set to `ask` still produces a RUNTIME question, but that
|
|
129
|
+
// answer lasts one run and is never persisted, so it is not this
|
|
130
|
+
// budget's business. Persisting it stays on the human path.
|
|
131
|
+
plan.skipped.push({ key, reason: 'class-c-guarded' });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fallback = defaultOf(key);
|
|
136
|
+
if (fallback === undefined) {
|
|
137
|
+
plan.skipped.push({ key, reason: 'unclassified' });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (interactive && plan.ask === null) {
|
|
142
|
+
plan.ask = key;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!isConservativeDefault(fallback)) {
|
|
147
|
+
// Taking a permissive value silently would be a decision made in
|
|
148
|
+
// the user's name — the one outcome the Iron Law names outright.
|
|
149
|
+
// Surfacing it as skipped forces the caller to deal with it instead
|
|
150
|
+
// of inheriting a permission nobody granted.
|
|
151
|
+
plan.skipped.push({ key, reason: 'non-conservative-default' });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
plan.silent.push({ key, value: fallback, command: changeCommand(key) });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return plan;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The end-summary block for a plan, or `null` when nothing was defaulted.
|
|
163
|
+
*
|
|
164
|
+
* `null` rather than an empty string so a caller cannot append a heading with no
|
|
165
|
+
* rows under it. This text rides INSIDE the single end-summary that
|
|
166
|
+
* `direct-answers` already requires — it is never a second summary.
|
|
167
|
+
*/
|
|
168
|
+
export function silentDefaultsSummary(plan: AskPlan): string | null {
|
|
169
|
+
if (plan.silent.length === 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const lines = plan.silent.map(
|
|
173
|
+
(s) => `- \`${s.key}\` → ${JSON.stringify(s.value)} (default) · change: \`${s.command}\``,
|
|
174
|
+
);
|
|
175
|
+
return `Settings resolved without asking this run:\n${lines.join('\n')}`;
|
|
176
|
+
}
|