@event4u/agent-config 9.24.0 → 9.26.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 +136 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +182 -0
- package/dist/agent-src/commands/analyze/inbox.md +33 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +194 -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/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +3 -2
- package/dist/agent-src/rules/design-fidelity.md +17 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +116 -0
- package/dist/agent-src/rules/evaluator-independence.md +111 -0
- package/dist/agent-src/rules/session-canary.md +15 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +176 -0
- package/dist/agent-src/rules/token-budget-discipline.md +6 -3
- package/dist/agent-src/rules/ui-audit-gate.md +3 -0
- package/dist/agent-src/rules/user-interaction.md +23 -0
- package/dist/agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/skills/rule-writing/SKILL.md +45 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +114 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +146 -28
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +16 -11
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +12 -7
- package/dist/hooks/dispatch.js +1501 -511
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/router.json +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/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/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/evaluator-budgets.json +68 -68
- package/src/config/gate-coverage.yml +131 -5
- package/src/config/gate-violation-baselines.json +11 -0
- 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 +12 -0
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +146 -5
- 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_depth_budget.ts +295 -0
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/conformance_scan.ts +422 -0
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/git_authorization_hook.ts +328 -0
- package/src/scripts/hook_manifest.yaml +69 -9
- package/src/scripts/hooks/block_no_verify.ts +3 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +371 -0
- package/src/scripts/hooks/concern_registry.ts +8 -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 +368 -0
- package/src/scripts/language_mirror_hook.ts +456 -0
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_decision_impact.ts +246 -0
- package/src/scripts/lint_example_fences.ts +609 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/lint_rule_interactions.ts +90 -0
- package/src/scripts/lint_rule_migration_ledger.ts +480 -0
- package/src/scripts/lint_skill_descriptions.ts +86 -2
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +38 -1
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/scripts/schemas/rule.schema.json +10 -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/settingsConsent.ts +136 -0
|
@@ -44,6 +44,17 @@
|
|
|
44
44
|
* construction and wake up exactly when a new skill lands close to an existing
|
|
45
45
|
* one. That is the point.
|
|
46
46
|
*
|
|
47
|
+
* A seventh check runs on EVERY skill and is neither absence nor presence — it
|
|
48
|
+
* is a prohibition:
|
|
49
|
+
*
|
|
50
|
+
* (g) preemption-phrase — the description claims priority over a sibling, or
|
|
51
|
+
* instructs activation regardless of the request.
|
|
52
|
+
* Unlike (a)-(f), this defect harms OTHER skills: it
|
|
53
|
+
* wins turns that belonged to them, and the loss is
|
|
54
|
+
* invisible at the source. See PREEMPTION_PATTERNS
|
|
55
|
+
* for why every pattern is anchored on the directive
|
|
56
|
+
* rather than on a vocabulary word.
|
|
57
|
+
*
|
|
47
58
|
* Deterministic, static-config only. Allowlist mirrors lint_skill_originality:
|
|
48
59
|
* `lint_skill_descriptions_allowlist.json`, capped at 20 (autonomous-execution
|
|
49
60
|
* allowlist-growth antipattern — over-cap means the linter is wrong).
|
|
@@ -78,6 +89,62 @@ const CONDITION_RE =
|
|
|
78
89
|
const IMPERATIVE_LEAD_RE =
|
|
79
90
|
/^["'`]*\s*(write|author|convert|review|run|add|build|generate|create|fix|wire|detect|analy[sz]e|optimi[sz]e|draft|extract|route|turn|produce|score|consolidate|rate|model|estimate|plan|check|audit|ship|emit|render|guide|poll|capture|explain|map|prepare|handle|manage|track|compare|validate|verify|enforce|choose|pick|enumerate|investigate|format|scaffold|design|define|describe|gate|block|refactor|migrate|transcribe|summari[sz]e|orchestrate|surface|propose|walk|stress-test|red-team|challenge|bootstrap|seed|install|deploy|package|split|merge|condense|harvest|evaluate|assess|diagnose|reproduce|trace|inspect|lint|test)/i;
|
|
80
91
|
|
|
92
|
+
/**
|
|
93
|
+
* (g) Preemption phrases — a description that argues for itself against the
|
|
94
|
+
* rest of the catalogue.
|
|
95
|
+
*
|
|
96
|
+
* Every other check here asks whether a description carries ENOUGH signal.
|
|
97
|
+
* This one is the opposite: a description claiming priority over a sibling, or
|
|
98
|
+
* instructing activation regardless of the request, degrades routing **globally
|
|
99
|
+
* and invisibly**. It does not misroute its own skill — it wins turns that
|
|
100
|
+
* belonged to other skills, and the loss surfaces as those skills mysteriously
|
|
101
|
+
* under-firing. The sweep behind this check found the class live in a shipped
|
|
102
|
+
* third-party description and in a second suite's own catalogue.
|
|
103
|
+
*
|
|
104
|
+
* Every pattern is anchored on the **directive**, never on a vocabulary word,
|
|
105
|
+
* and that is the whole design. A first pass keyed on the words alone
|
|
106
|
+
* (`overrides`, `supersedes`, `for every …`) returned three hits on this corpus
|
|
107
|
+
* and **all three were false positives**: `override-management` manages
|
|
108
|
+
* overrides, `decision-review` asks whether an ADR should be superseded, and
|
|
109
|
+
* `prediction-pool-optimizer` answers "every question" in a prediction pool.
|
|
110
|
+
* Those are subjects, not claims. The patterns below therefore require the
|
|
111
|
+
* shape of an instruction to the router (`always use`, `use this instead of`,
|
|
112
|
+
* `this skill takes precedence`), which no domain noun produces.
|
|
113
|
+
*/
|
|
114
|
+
const PREEMPTION_PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
115
|
+
[
|
|
116
|
+
'unconditional activation',
|
|
117
|
+
/\b(?:always|invariably|unconditionally)\s+(?:use|load|apply|run|activate|invoke|prefer)\b/i,
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
'activation regardless of the request',
|
|
121
|
+
/\b(?:regardless of|no matter)\s+(?:the|what|which)\b|\b(?:for|on)\s+(?:every|all|any)\s+(?:request|turn|prompt|conversation|session)s?\b|\beven if not (?:asked|requested)\b/i,
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
'priority over a sibling',
|
|
125
|
+
/\b(?:use|load|apply|invoke|prefer)\s+(?:this|these|it)\b[^.;]{0,60}?\b(?:instead of|rather than|over|in preference to)\b/i,
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
'authority claim over the catalogue',
|
|
129
|
+
/\bthis (?:skill|one|artifact)\s+(?:takes precedence|outranks|supersedes|overrides|wins)\b|\b(?:takes precedence over|outranks)\s+(?:all|any|every)\b/i,
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
'load-order claim',
|
|
133
|
+
/\b(?:before|prior to)\s+(?:any|all)\s+other\s+(?:skill|tool|rule)s?\b|\bload (?:this )?first\b/i,
|
|
134
|
+
],
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/** The first preemption claim in a description, or null. */
|
|
138
|
+
function preemptionPhrase(desc: string): { kind: string; match: string } | null {
|
|
139
|
+
for (const [kind, rx] of PREEMPTION_PATTERNS) {
|
|
140
|
+
const m = rx.exec(desc);
|
|
141
|
+
if (m) {
|
|
142
|
+
return { kind: kind as string, match: m[0] };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
81
148
|
interface Violation {
|
|
82
149
|
slug: string;
|
|
83
150
|
code:
|
|
@@ -86,7 +153,8 @@ interface Violation {
|
|
|
86
153
|
| 'triggers-are-name'
|
|
87
154
|
| 'no-routing-signal'
|
|
88
155
|
| 'clustered-no-sibling-routing'
|
|
89
|
-
| 'clustered-no-quoted-phrase'
|
|
156
|
+
| 'clustered-no-quoted-phrase'
|
|
157
|
+
| 'preemption-phrase';
|
|
90
158
|
detail: string;
|
|
91
159
|
}
|
|
92
160
|
|
|
@@ -204,6 +272,22 @@ function analyseSkill(slug: string, fm: Record<string, string>, siblings: readon
|
|
|
204
272
|
});
|
|
205
273
|
}
|
|
206
274
|
|
|
275
|
+
// (g) a claim of priority over the rest of the catalogue. Unlike (a)-(d),
|
|
276
|
+
// this defect harms OTHER skills, so it is checked on every skill and is
|
|
277
|
+
// never scoped to a cluster — a preemption phrase costs turns regardless of
|
|
278
|
+
// whether the router already had a near-neighbour to confuse it with.
|
|
279
|
+
const preemption = preemptionPhrase(desc);
|
|
280
|
+
if (preemption) {
|
|
281
|
+
out.push({
|
|
282
|
+
slug,
|
|
283
|
+
code: 'preemption-phrase',
|
|
284
|
+
detail:
|
|
285
|
+
`description claims ${preemption.kind} ("${preemption.match}") — a description ` +
|
|
286
|
+
'argues for its own routing conditions, never against its siblings; this wins ' +
|
|
287
|
+
'turns that belonged to other skills and the loss is invisible at the source',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
207
291
|
// (e) + (f) fire ONLY for skills the canonical tool put in a cluster.
|
|
208
292
|
if (siblings.length > 0) {
|
|
209
293
|
const unrouted = siblings.filter((s) => !desc.includes(s));
|
|
@@ -332,4 +416,4 @@ if (path.resolve(process.argv[1] ?? '') === path.resolve(_HERE)) {
|
|
|
332
416
|
process.exit(main(process.argv.slice(2)));
|
|
333
417
|
}
|
|
334
418
|
|
|
335
|
-
export { analyseSkill, normalize, triggerPhrases, main };
|
|
419
|
+
export { analyseSkill, normalize, triggerPhrases, preemptionPhrase, main };
|
|
@@ -29,6 +29,20 @@ import fs from 'node:fs';
|
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
// This hook is the first in the tree to import from `src/shared/`, and its
|
|
33
|
+
// contract is fail-open — but the `try/catch` that delivers that lives in
|
|
34
|
+
// `main()`, so an import-time throw would escape it. The concern is real and
|
|
35
|
+
// the answer is measured rather than assumed: esbuild INLINES both modules into
|
|
36
|
+
// `dist/hooks/dispatch.js` (verified by building the bundle to a scratch path
|
|
37
|
+
// and grepping for `consentVerdict` and `withheld-machine-inferred`), so the
|
|
38
|
+
// installed path performs no runtime module resolution at all. Both modules are
|
|
39
|
+
// also pure declarations with zero top-level side effects, so there is nothing
|
|
40
|
+
// at import time that can throw. If either fact stops holding — a dynamic
|
|
41
|
+
// import, a module-level read — the fail-open guarantee weakens and this
|
|
42
|
+
// import has to move inside the guard.
|
|
43
|
+
import type { SettingsClass } from '../shared/settingsClasses.js';
|
|
44
|
+
import { consentVerdict, type ConsentVerdict } from '../shared/settingsConsent.js';
|
|
45
|
+
|
|
32
46
|
import {
|
|
33
47
|
buildSidecar,
|
|
34
48
|
LESSONS_NAME,
|
|
@@ -40,18 +54,54 @@ const SETTINGS_FILE = '.agent-settings.yml';
|
|
|
40
54
|
/** Wall-clock budget for the whole aggregation (ms). */
|
|
41
55
|
export const BUDGET_MS = 2000;
|
|
42
56
|
|
|
57
|
+
/** The key this hook is gated on. */
|
|
58
|
+
export const LEARN_KEY = 'memory.learn_on_session_end';
|
|
59
|
+
|
|
43
60
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
61
|
+
* The key's class, pinned rather than parsed.
|
|
62
|
+
*
|
|
63
|
+
* `classOfPath` would give the same answer, but only after reading and parsing
|
|
64
|
+
* `docs/contracts/settings-classes.md` — a multi-hundred-row markdown file — on
|
|
65
|
+
* every session teardown, which is exactly the cost the 2 s budget above exists
|
|
66
|
+
* to avoid. The drift risk a hardcoded constant carries is neutralised the
|
|
67
|
+
* cheap way instead: `tests/scripts/memory_learn_hook.test.ts` asserts the
|
|
68
|
+
* contract still classifies this key `B`, so a reclassification reds CI rather
|
|
69
|
+
* than silently unbinding the gate.
|
|
47
70
|
*/
|
|
48
|
-
export
|
|
71
|
+
export const LEARN_KEY_CLASS: SettingsClass = 'B';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether `memory.learn_on_session_end` reads as enabled, or `undefined` when
|
|
75
|
+
* the file or the key is absent (same hand-rolled mini-parser pattern as the
|
|
76
|
+
* sibling hooks — no YAML dependency in the hook hot path).
|
|
77
|
+
*
|
|
78
|
+
* Normalised strictly: the literal `true` and nothing else enables the hook. A
|
|
79
|
+
* crude parser must not let `yes` / `1` / `maybe` read as a permission, and
|
|
80
|
+
* `isConservativeDefault` would treat any non-empty string as permissive — so
|
|
81
|
+
* the normalisation happens here, before the consent check, not inside it.
|
|
82
|
+
*
|
|
83
|
+
* WHAT THE TRI-STATE DOES AND DOES NOT SAY. `undefined` means "no answer was
|
|
84
|
+
* found" — the key is absent, the file is absent, OR the file could not be read
|
|
85
|
+
* at all (permissions, a directory at the path, an I/O error). `false` means
|
|
86
|
+
* EITHER a deliberate `false` OR a present-but-malformed scalar. So there are
|
|
87
|
+
* two collapses, not one: a user who writes `learn_on_session_end: yes` and a
|
|
88
|
+
* user whose settings file is unreadable both get a silent, permanent no-op
|
|
89
|
+
* with no diagnostic.
|
|
90
|
+
*
|
|
91
|
+
* The fail-safe direction is deliberate in both cases — a hook that writes
|
|
92
|
+
* files must not be enabled by a value nobody typed on purpose, nor by a file
|
|
93
|
+
* nobody could read. The ambiguity is the cost, and it is stated here rather
|
|
94
|
+
* than implied by a "raw scalar" this function does not actually return.
|
|
95
|
+
* Diagnosing either shape belongs to `settings:check`, not to a fail-open
|
|
96
|
+
* teardown hook.
|
|
97
|
+
*/
|
|
98
|
+
export function readLearnValue(root: string): boolean | undefined {
|
|
49
99
|
const p = path.join(root, SETTINGS_FILE);
|
|
50
100
|
let text: string;
|
|
51
101
|
try {
|
|
52
102
|
text = fs.readFileSync(p, 'utf8');
|
|
53
103
|
} catch {
|
|
54
|
-
return
|
|
104
|
+
return undefined;
|
|
55
105
|
}
|
|
56
106
|
let inMemory = false;
|
|
57
107
|
for (const rawLine of text.split('\n')) {
|
|
@@ -69,7 +119,60 @@ export function enabled(root: string): boolean {
|
|
|
69
119
|
if (m) return m[1] === 'true';
|
|
70
120
|
}
|
|
71
121
|
}
|
|
72
|
-
return
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The consent verdict for running the aggregation.
|
|
127
|
+
*
|
|
128
|
+
* This is the first production caller of `consentVerdict` — Phase 5 step 4 of
|
|
129
|
+
* `road-to-zero-ceremony-settings` asks the consent-gated action to verify the
|
|
130
|
+
* recorded DECISION rather than read the bare value, and this session_end
|
|
131
|
+
* concern is the only action in the tree that a class-B key actually gates.
|
|
132
|
+
*
|
|
133
|
+
* `handEdited: true` is a fact about the path, not a shortcut: the file read
|
|
134
|
+
* here is the PROJECT-LOCAL `.agent-settings.yml`, which
|
|
135
|
+
* `docs/contracts/settings-classes.md` names as a file only a human writes —
|
|
136
|
+
* `settings:set` and the GUI both write the user-global file instead. So on
|
|
137
|
+
* this path a permissive value IS the recorded decision and the verdict is
|
|
138
|
+
* `granted`.
|
|
139
|
+
*
|
|
140
|
+
* WHAT THIS CHANGES TODAY: nothing observable. Stated plainly because the
|
|
141
|
+
* opposite was claimed first and it was wrong. With `cls` a constant,
|
|
142
|
+
* `handEdited` a constant `true`, and the value already reduced to a boolean by
|
|
143
|
+
* `readLearnValue`, every discriminating input to `consentVerdict` is fixed, so
|
|
144
|
+
* `enabled()` returns exactly what the previous `=== 'true'` read returned —
|
|
145
|
+
* including for `yes` / `1` / `on`, which that read already rejected. The
|
|
146
|
+
* normalisation is not new and this call did not narrow it.
|
|
147
|
+
*
|
|
148
|
+
* WHAT IT DOES BUY, which is smaller than it sounds and worth being exact
|
|
149
|
+
* about: the obligation is expressed in code instead of prose, so
|
|
150
|
+
* `consentVerdict` has a real caller and stops being a library nobody invokes;
|
|
151
|
+
* the class binding is asserted against the contract by a test, so a
|
|
152
|
+
* reclassification reds CI (it does NOT re-route the hook at runtime — the
|
|
153
|
+
* class is a hardcoded constant); and the day this hook learns to read the
|
|
154
|
+
* user-global file, the sidecar check is already the thing standing in front of
|
|
155
|
+
* it rather than a change someone must remember to make.
|
|
156
|
+
*
|
|
157
|
+
* KNOWN GAP, recorded rather than fixed here: the hook reads only the
|
|
158
|
+
* project-local file, so a user who enables this in the GUI — which writes the
|
|
159
|
+
* user-global file plus the provenance sidecar — never triggers it. Reading the
|
|
160
|
+
* user-global file is where the sidecar's `auto-detected`-refusing power would
|
|
161
|
+
* actually discriminate, but it is also a behaviour change (a hook that writes
|
|
162
|
+
* files would start firing for users it does not fire for today), so it belongs
|
|
163
|
+
* to a decision rather than to this step. Filed in the roadmap.
|
|
164
|
+
*/
|
|
165
|
+
export function learnConsent(root: string): ConsentVerdict {
|
|
166
|
+
return consentVerdict({
|
|
167
|
+
cls: LEARN_KEY_CLASS,
|
|
168
|
+
value: readLearnValue(root) ?? false,
|
|
169
|
+
handEdited: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `true` only when the recorded decision permits the aggregation. */
|
|
174
|
+
export function enabled(root: string): boolean {
|
|
175
|
+
return learnConsent(root) === 'granted';
|
|
73
176
|
}
|
|
74
177
|
|
|
75
178
|
/** Run the aggregation. Returns the one-line marker to emit, or null. */
|
|
@@ -66,6 +66,20 @@ function _isObject(v: unknown): v is JsonObject {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Return [tool_name, command_text] from a tool-event payload. */
|
|
69
|
+
/**
|
|
70
|
+
* Unwrap the dispatcher envelope.
|
|
71
|
+
*
|
|
72
|
+
* The dispatcher hands every concern `{event, platform, payload: <native>}`,
|
|
73
|
+
* but this hook read `tool_name` / `tool_response` off the TOP level. Fed a raw
|
|
74
|
+
* payload it worked; fed the real envelope it found nothing and returned ALLOW.
|
|
75
|
+
* Measured 2026-08-06: standalone 624 bytes of output, through the built
|
|
76
|
+
* dispatcher 0 bytes — the reminder had never fired in production.
|
|
77
|
+
*/
|
|
78
|
+
function _unwrap(envelope: JsonObject): JsonObject {
|
|
79
|
+
const inner = envelope["payload"];
|
|
80
|
+
return _isObject(inner) ? inner : envelope;
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function _extractCommand(payload: JsonObject): [string | null, string | null] {
|
|
70
84
|
const toolRaw = payload["tool_name"] ?? payload["toolName"] ?? payload["tool"];
|
|
71
85
|
const tool = typeof toolRaw === "string" ? toolRaw : null;
|
|
@@ -124,12 +138,13 @@ export function main(): number {
|
|
|
124
138
|
return EXIT_ALLOW;
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
const
|
|
141
|
+
const inner = _unwrap(envelope);
|
|
142
|
+
const [tool, command] = _extractCommand(inner);
|
|
128
143
|
if (!_isPrCreate(tool, command)) {
|
|
129
144
|
return EXIT_ALLOW;
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
const match = _PR_URL.exec(_toolOutput(
|
|
147
|
+
const match = _PR_URL.exec(_toolOutput(inner));
|
|
133
148
|
if (match === null) {
|
|
134
149
|
return EXIT_ALLOW; // create attempted but no URL produced (failed / dry-run) — do not nag
|
|
135
150
|
}
|
package/src/scripts/release.ts
CHANGED
|
@@ -767,11 +767,48 @@ function watch_pr_checks(branch: string): void {
|
|
|
767
767
|
* 2026-08-03, 9.15.0). Same-branch commits are never ours to drop, so the
|
|
768
768
|
* recovery is merge-and-retry, never force-push.
|
|
769
769
|
*/
|
|
770
|
+
/**
|
|
771
|
+
* True when git rejected the push because the remote ref moved ahead — the
|
|
772
|
+
* ONLY failure the merge-and-retry recovery below can repair.
|
|
773
|
+
*
|
|
774
|
+
* git's wording for this is stable across versions: the hint is `fetch first`
|
|
775
|
+
* / `Updates were rejected`, and the ref line carries `[rejected]` with
|
|
776
|
+
* `non-fast-forward` or `fetch first`.
|
|
777
|
+
*/
|
|
778
|
+
export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
|
|
779
|
+
const text = `${stderr}\n${stdout}`;
|
|
780
|
+
return (
|
|
781
|
+
/\[rejected\]/i.test(text) &&
|
|
782
|
+
/(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text)
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
770
786
|
function push_release_branch(branch: string): void {
|
|
771
|
-
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false });
|
|
787
|
+
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });
|
|
772
788
|
if (first.returncode === 0) {
|
|
789
|
+
process.stdout.write(first.stdout);
|
|
790
|
+
process.stderr.write(first.stderr);
|
|
773
791
|
return;
|
|
774
792
|
}
|
|
793
|
+
|
|
794
|
+
// A push can fail for many reasons — a pre-push hook refusing the commit,
|
|
795
|
+
// no credentials, a protected branch. Only ONE of them is repaired by
|
|
796
|
+
// fetch+merge+retry, and the recovery's first step (`git fetch <branch>`)
|
|
797
|
+
// exits 128 for a branch that was never pushed. Taking that path
|
|
798
|
+
// unconditionally replaced every real error with
|
|
799
|
+
// `couldn't find remote ref <branch>` — measured 2026-08-06 on 9.26.0,
|
|
800
|
+
// where a pre-push gate rejection surfaced as a git-fetch crash and cost
|
|
801
|
+
// an hour of diagnosis pointed at the wrong layer.
|
|
802
|
+
if (!_is_non_fast_forward(first.stderr, first.stdout)) {
|
|
803
|
+
process.stdout.write(first.stdout);
|
|
804
|
+
process.stderr.write(first.stderr);
|
|
805
|
+
die(
|
|
806
|
+
`push of ${branch} failed (exit ${first.returncode}) and the remote ref did not move — ` +
|
|
807
|
+
'this is not the fetch-and-retry case. The push output above is the real error ' +
|
|
808
|
+
'(a pre-push gate, credentials, or branch protection); fix that and re-run.',
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
775
812
|
process.stdout.write(`↻ ${REMOTE}/${branch} moved — integrating and retrying push\n`);
|
|
776
813
|
run(['git', 'fetch', REMOTE, branch]);
|
|
777
814
|
run(['git', 'merge', '--no-edit', `${REMOTE}/${branch}`]);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "review-findings.schema.json",
|
|
4
|
+
"title": "Review findings",
|
|
5
|
+
"description": "ONE findings shape for both review tracks. All six external reviewers of 9.18.1 converged on a single ask: the R2 gate hand-parses Markdown and carries `unbalanced-fence` and `malformed-row` as first-class violation kinds — i.e. it has defect classes FOR ITS OWN PARSER. This schema is deliberately NOT a new format: it is the shape `self_review_gate.ts` already emits under `--findings-out` and `check_finding_dispositions.ts` already hand-validates in code, lifted into a file both tracks can point at. Inventing a second JSON shape here would have been the exact fragmentation the reviewers complained about.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"findings"
|
|
10
|
+
],
|
|
11
|
+
"additionalProperties": true,
|
|
12
|
+
"properties": {
|
|
13
|
+
"schema_version": {
|
|
14
|
+
"enum": [
|
|
15
|
+
1
|
|
16
|
+
],
|
|
17
|
+
"description": "Pinned. `check_finding_dispositions.parse_ledger` already rejects anything else, so this is a promotion of an existing check rather than a new constraint. Spelled as a one-member `enum` rather than `const`: the repo's own Draft-07 subset validator (validate_frontmatter.ts) enforces `enum` and silently ignores `const`, so `const` here would have been a pin nothing checked."
|
|
18
|
+
},
|
|
19
|
+
"release": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Version the ledger belongs to. Present on the release-findings ledger (agents/evidence/release-findings/<version>.json); absent on a bare --findings-out emission, which is why it is not required here."
|
|
22
|
+
},
|
|
23
|
+
"findings": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"description": "May be empty. An empty array is an honest null, not a missing field — the distinction the Markdown track spells with its own §2.3 line. The item shape is INLINED rather than `$ref`-ed for the same measured reason: the subset validator does not resolve `$ref`, so a referenced definition would have validated nothing.",
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": [
|
|
29
|
+
"severity",
|
|
30
|
+
"kind",
|
|
31
|
+
"title"
|
|
32
|
+
],
|
|
33
|
+
"additionalProperties": true,
|
|
34
|
+
"properties": {
|
|
35
|
+
"finding_id": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^[0-9a-f]{12}$",
|
|
38
|
+
"description": "First 12 hex of sha256(`kind|title|file`), per findingId() in self_review_gate.ts. Stable across runs, which is what lets the disposition ledger key on it. NOT required: an in-flight finding may not have been keyed yet, and requiring it would make the schema reject the shape the model returns before the gate stamps it."
|
|
39
|
+
},
|
|
40
|
+
"severity": {
|
|
41
|
+
"enum": [
|
|
42
|
+
"critical",
|
|
43
|
+
"high",
|
|
44
|
+
"medium",
|
|
45
|
+
"low"
|
|
46
|
+
],
|
|
47
|
+
"description": "The JSON track's four levels. The Markdown track's own severity vocabulary is a superset in spirit but a different list; mapping between them is a caller's job, not this schema's — see the mapping note in the description of `kind`."
|
|
48
|
+
},
|
|
49
|
+
"kind": {
|
|
50
|
+
"enum": [
|
|
51
|
+
"security",
|
|
52
|
+
"claim",
|
|
53
|
+
"correctness",
|
|
54
|
+
"style"
|
|
55
|
+
],
|
|
56
|
+
"description": "Finding class. Load-bearing for the id: findingId() hashes kind|title|file, so a kind change is a new finding rather than an edit of the old one."
|
|
57
|
+
},
|
|
58
|
+
"title": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"minLength": 1,
|
|
61
|
+
"maxLength": 300,
|
|
62
|
+
"description": "Truncated at 300 by parseFindings() in self_review_gate.ts; the bound is pinned here so a producer cannot emit a title the consumer will silently cut."
|
|
63
|
+
},
|
|
64
|
+
"detail": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"maxLength": 2000,
|
|
67
|
+
"description": "Truncated at 2000 by parseFindings(). Same reasoning as `title`."
|
|
68
|
+
},
|
|
69
|
+
"file": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Repo-relative path. Optional because a finding can be about the change as a whole; findingId() folds an absent file to the empty string so the id stays stable either way."
|
|
72
|
+
},
|
|
73
|
+
"status": {
|
|
74
|
+
"enum": [
|
|
75
|
+
"open",
|
|
76
|
+
"fixed",
|
|
77
|
+
"accepted_risk",
|
|
78
|
+
"false_positive"
|
|
79
|
+
],
|
|
80
|
+
"description": "Disposition. `fixed` / `false_positive` / `accepted_risk` are the three terminal states check_finding_dispositions accepts; `open` is the initial state. NOTE the underscore spelling — the Markdown track writes `accepted-risk` with a hyphen in its Status cell, and the two vocabularies are deliberately not unified here: renaming either would break committed artefacts on that track."
|
|
81
|
+
},
|
|
82
|
+
"rationale": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Why the disposition. Required by check_finding_dispositions for a non-`fixed` terminal status; enforced there rather than here because the requirement is conditional on the workflow stage, not on the document."
|
|
85
|
+
},
|
|
86
|
+
"verified_by": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Who or what confirmed the disposition."
|
|
89
|
+
},
|
|
90
|
+
"date": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
|
|
93
|
+
"description": "ISO calendar date of the disposition."
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
"enum": ["package", "project"],
|
|
19
19
|
"default": "package"
|
|
20
20
|
},
|
|
21
|
+
"decision_impact": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": ["overrides-model-default", "encodes-house-choice", "already-complied-with"],
|
|
24
|
+
"description": "What is at stake when this rule fires — distinct from `type`, which is a DELIVERY class (how the rule reaches the model). `overrides-model-default`: the model's base behaviour is wrong here, so the rule is load-bearing. `encodes-house-choice`: the model would make *a* defensible choice but not *our* choice. `already-complied-with`: the model does this unprompted — the one value that permits removal, and therefore the one carrying an evidence bar (`decision_impact_evidence`, enforced by lint_decision_impact). OPTIONAL by design: classifying all 111 rules in one batch is exactly the shape that trips check_kernel_prefix_stability, so it lands on new rules and on rules touched for another reason."
|
|
25
|
+
},
|
|
26
|
+
"decision_impact_evidence": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 20,
|
|
29
|
+
"description": "Required when `decision_impact: already-complied-with`, forbidden otherwise. A pointer a reviewer can follow to the observation that the model complies without this rule — an eval report, a PR discussion, a trajectory sample. The bar exists because that classification is what permits deleting a rule, and 'the model probably does this anyway' is the cheapest possible way to remove a floor that was working precisely because nothing had crossed it."
|
|
30
|
+
},
|
|
21
31
|
"description": {
|
|
22
32
|
"type": "string",
|
|
23
33
|
"minLength": 1,
|
|
@@ -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
|
+
}
|