@event4u/agent-config 9.28.0 → 9.29.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/AGENTS.md +6 -6
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/delegation-policy.md +11 -7
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +4 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
- 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 +6 -43
- package/dist/cli-delegate/cmd_doctor.js +9 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- 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 +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- 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_settings_get.js +1 -7
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- 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 +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +69 -20
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +373 -35
- package/dist/install/install.mjs +42 -17
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +651 -262
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +39 -9
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_lib/agent_settings.ts +58 -0
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/self_repair.ts +160 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/orchestration_record.ts +8 -0
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/routing_doctor.ts +21 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +287 -57
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
* instruction lifts that floor. Running this command IS the user's word.
|
|
13
13
|
*
|
|
14
14
|
* Route selection follows the spec exactly: a pull request when the fix can be
|
|
15
|
-
* authored and pushed
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* authored and pushed — directly when the pre-flight probe shows upstream push
|
|
16
|
+
* rights, via fork → cross-repo PR when it does not — an issue when it cannot,
|
|
17
|
+
* and a local record when nothing can leave the machine. Once publishing was
|
|
18
|
+
* authorized, failures degrade down the ladder WITHIN the same call (push/PR
|
|
19
|
+
* failure or 30 s timeout → issue; issue failure → the record stays open with
|
|
20
|
+
* the errors attached) — degrading never widens what leaves the machine, only
|
|
21
|
+
* the vehicle. `--dry-run` prints the resolved route and the rendered body and
|
|
22
|
+
* touches no network — that is the path the tests drive.
|
|
18
23
|
*
|
|
19
24
|
* Fail-closed on privacy: the record's own text goes through the audited
|
|
20
25
|
* privacy floor before anything leaves the machine, and a refusal downgrades
|
|
@@ -26,17 +31,31 @@ import * as path from 'node:path';
|
|
|
26
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
32
|
|
|
28
33
|
import {
|
|
29
|
-
|
|
34
|
+
chooseEgress,
|
|
30
35
|
type DefectRecord,
|
|
31
36
|
type EgressCapability,
|
|
32
37
|
type EgressRoute,
|
|
33
38
|
egressBlockedReason,
|
|
39
|
+
type PushVia,
|
|
34
40
|
renderReport,
|
|
41
|
+
sanitizeEvidence,
|
|
35
42
|
} from './_lib/self_repair.js';
|
|
36
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
attachReleaseErrors,
|
|
45
|
+
listRecords,
|
|
46
|
+
markReleased,
|
|
47
|
+
openRecords,
|
|
48
|
+
readRecord,
|
|
49
|
+
} from './_lib/self_repair_store.js';
|
|
37
50
|
|
|
38
51
|
export const UPSTREAM_REPO = 'event4u-app/agent-config';
|
|
39
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Per-step ceiling for every network-touching egress step. A hung `git push`
|
|
55
|
+
* or `gh` call degrades down the ladder instead of hanging the release.
|
|
56
|
+
*/
|
|
57
|
+
export const EGRESS_STEP_TIMEOUT_MS = 30_000;
|
|
58
|
+
|
|
40
59
|
const EXIT_OK = 0;
|
|
41
60
|
const EXIT_FAIL = 1;
|
|
42
61
|
|
|
@@ -44,12 +63,30 @@ const EXIT_FAIL = 1;
|
|
|
44
63
|
export interface Probe {
|
|
45
64
|
agentConfigCheckout: string | null;
|
|
46
65
|
ghAuthenticated: boolean;
|
|
47
|
-
|
|
66
|
+
canPushUpstream: boolean;
|
|
67
|
+
canFork: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RunResult {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
out: string;
|
|
73
|
+
timedOut: boolean;
|
|
48
74
|
}
|
|
49
75
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
/** Injectable command runner — tests script it, production shells out. */
|
|
77
|
+
export type Runner = (cmd: string, args: string[], cwd?: string) => RunResult;
|
|
78
|
+
|
|
79
|
+
function run(cmd: string, args: string[], cwd?: string): RunResult {
|
|
80
|
+
const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8', timeout: EGRESS_STEP_TIMEOUT_MS });
|
|
81
|
+
// Only spawnSync's own ETIMEDOUT counts: a child killed externally by
|
|
82
|
+
// SIGTERM is a failure, not a timeout, and recording it as "timed out"
|
|
83
|
+
// points the next debugging session at the wrong cause (R2 finding #3).
|
|
84
|
+
const timedOut = (r.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
|
|
85
|
+
return {
|
|
86
|
+
ok: r.status === 0 && !timedOut,
|
|
87
|
+
out: `${r.stdout ?? ''}${r.stderr ?? ''}`,
|
|
88
|
+
timedOut,
|
|
89
|
+
};
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
/**
|
|
@@ -71,18 +108,63 @@ export function findAgentConfigCheckout(env: NodeJS.ProcessEnv, cwd: string): st
|
|
|
71
108
|
return null;
|
|
72
109
|
}
|
|
73
110
|
|
|
74
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Pre-flight probe of ACTUAL push rights, not remote existence. `git remote`
|
|
113
|
+
* returning a name proves only that the repo was cloned — every consumer of
|
|
114
|
+
* the public repo has that and no write access. `gh api repos/<repo>` returns
|
|
115
|
+
* the authenticated viewer's `permissions.push` and the repo's `allow_forking`,
|
|
116
|
+
* which are the two facts the route actually depends on. An unreachable or
|
|
117
|
+
* unauthenticated API answers `false` for both — the ladder then degrades to
|
|
118
|
+
* an issue rather than failing at push time.
|
|
119
|
+
*/
|
|
120
|
+
export function probeUpstreamRights(
|
|
121
|
+
runner: Runner,
|
|
122
|
+
repo: string,
|
|
123
|
+
): { canPushUpstream: boolean; canFork: boolean } {
|
|
124
|
+
const r = runner('gh', ['api', `repos/${repo}`]);
|
|
125
|
+
if (!r.ok) {
|
|
126
|
+
return { canPushUpstream: false, canFork: false };
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const parsed: unknown = JSON.parse(r.out);
|
|
130
|
+
const meta = parsed as {
|
|
131
|
+
permissions?: { push?: boolean };
|
|
132
|
+
allow_forking?: boolean;
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
canPushUpstream: meta.permissions?.push === true,
|
|
136
|
+
canFork: meta.allow_forking !== false,
|
|
137
|
+
};
|
|
138
|
+
} catch {
|
|
139
|
+
return { canPushUpstream: false, canFork: false };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function probeMachine(
|
|
144
|
+
env: NodeJS.ProcessEnv,
|
|
145
|
+
cwd: string,
|
|
146
|
+
runner: Runner = run,
|
|
147
|
+
repo: string = UPSTREAM_REPO,
|
|
148
|
+
): Probe {
|
|
75
149
|
const checkout = findAgentConfigCheckout(env, cwd);
|
|
76
|
-
const gh =
|
|
77
|
-
const
|
|
78
|
-
|
|
150
|
+
const gh = runner('gh', ['auth', 'status']);
|
|
151
|
+
const rights = gh.ok
|
|
152
|
+
? probeUpstreamRights(runner, repo)
|
|
153
|
+
: { canPushUpstream: false, canFork: false };
|
|
154
|
+
return {
|
|
155
|
+
agentConfigCheckout: checkout,
|
|
156
|
+
ghAuthenticated: gh.ok,
|
|
157
|
+
canPushUpstream: rights.canPushUpstream,
|
|
158
|
+
canFork: gh.ok && rights.canFork,
|
|
159
|
+
};
|
|
79
160
|
}
|
|
80
161
|
|
|
81
162
|
export function capabilityOf(probe: Probe): EgressCapability {
|
|
82
163
|
return {
|
|
83
164
|
hasAgentConfigCheckout: probe.agentConfigCheckout !== null,
|
|
84
165
|
ghAuthenticated: probe.ghAuthenticated,
|
|
85
|
-
|
|
166
|
+
canPushUpstream: probe.canPushUpstream,
|
|
167
|
+
canFork: probe.canFork,
|
|
86
168
|
};
|
|
87
169
|
}
|
|
88
170
|
|
|
@@ -93,6 +175,7 @@ export function titleFor(record: DefectRecord): string {
|
|
|
93
175
|
|
|
94
176
|
export interface ReleasePlan {
|
|
95
177
|
route: EgressRoute;
|
|
178
|
+
pushVia: PushVia | null;
|
|
96
179
|
blocked: string | null;
|
|
97
180
|
title: string;
|
|
98
181
|
body: string;
|
|
@@ -101,9 +184,177 @@ export interface ReleasePlan {
|
|
|
101
184
|
/** Pure: everything `release` decides before it touches the network. */
|
|
102
185
|
export function planRelease(record: DefectRecord, probe: Probe, repoRoot: string): ReleasePlan {
|
|
103
186
|
const blocked = egressBlockedReason(record, repoRoot);
|
|
104
|
-
const
|
|
105
|
-
blocked !== null
|
|
106
|
-
|
|
187
|
+
const choice =
|
|
188
|
+
blocked !== null
|
|
189
|
+
? { route: 'local-only' as const, pushVia: null }
|
|
190
|
+
: chooseEgress(capabilityOf(probe));
|
|
191
|
+
return {
|
|
192
|
+
route: choice.route,
|
|
193
|
+
pushVia: choice.pushVia,
|
|
194
|
+
blocked,
|
|
195
|
+
title: titleFor(record),
|
|
196
|
+
body: renderReport(record, choice.route),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── egress ladder execution ────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/** One egress step's outcome — kept for the record when the ladder exhausts. */
|
|
203
|
+
export interface EgressAttempt {
|
|
204
|
+
step: string;
|
|
205
|
+
ok: boolean;
|
|
206
|
+
detail: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ReleaseOutcome {
|
|
210
|
+
/** What actually got published, or null when the whole ladder failed. */
|
|
211
|
+
published: 'pull-request' | 'issue' | null;
|
|
212
|
+
attempts: EgressAttempt[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface ReleaseDeps {
|
|
216
|
+
runner: Runner;
|
|
217
|
+
now: () => string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const FORK_REMOTE = 'self-repair-fork';
|
|
221
|
+
|
|
222
|
+
function failDetail(step: string, r: RunResult): string {
|
|
223
|
+
return r.timedOut
|
|
224
|
+
? `${step}: timed out after ${EGRESS_STEP_TIMEOUT_MS / 1000}s`
|
|
225
|
+
: `${step}: ${r.out.trim() || 'failed'}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Push the fix branch — directly, or via fork → cross-repo PR — then open the
|
|
230
|
+
* PR. Returns null on success; on any step failure or timeout returns the
|
|
231
|
+
* failed attempt so the caller degrades down the ladder.
|
|
232
|
+
*/
|
|
233
|
+
function tryPullRequest(
|
|
234
|
+
plan: ReleasePlan,
|
|
235
|
+
probe: Probe,
|
|
236
|
+
repo: string,
|
|
237
|
+
branch: string,
|
|
238
|
+
via: PushVia,
|
|
239
|
+
deps: ReleaseDeps,
|
|
240
|
+
): EgressAttempt | null {
|
|
241
|
+
const checkout = probe.agentConfigCheckout!;
|
|
242
|
+
let prHead: string | null = null;
|
|
243
|
+
|
|
244
|
+
if (via === 'upstream') {
|
|
245
|
+
const pushed = deps.runner('git', ['push', '-u', 'origin', branch], checkout);
|
|
246
|
+
if (!pushed.ok) {
|
|
247
|
+
return { step: 'push-upstream', ok: false, detail: failDetail('push-upstream', pushed) };
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
const forked = deps.runner('gh', ['repo', 'fork', repo, '--clone=false']);
|
|
251
|
+
if (!forked.ok) {
|
|
252
|
+
return { step: 'fork-ensure', ok: false, detail: failDetail('fork-ensure', forked) };
|
|
253
|
+
}
|
|
254
|
+
const who = deps.runner('gh', ['api', 'user', '--jq', '.login']);
|
|
255
|
+
if (!who.ok || who.out.trim().length === 0) {
|
|
256
|
+
return { step: 'whoami', ok: false, detail: failDetail('whoami', who) };
|
|
257
|
+
}
|
|
258
|
+
const login = who.out.trim();
|
|
259
|
+
const repoName = repo.split('/')[1] ?? repo;
|
|
260
|
+
const hasRemote = deps.runner('git', ['remote', 'get-url', FORK_REMOTE], checkout);
|
|
261
|
+
if (!hasRemote.ok) {
|
|
262
|
+
const added = deps.runner(
|
|
263
|
+
'git',
|
|
264
|
+
['remote', 'add', FORK_REMOTE, `https://github.com/${login}/${repoName}.git`],
|
|
265
|
+
checkout,
|
|
266
|
+
);
|
|
267
|
+
if (!added.ok) {
|
|
268
|
+
return { step: 'fork-remote', ok: false, detail: failDetail('fork-remote', added) };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const pushed = deps.runner('git', ['push', '-u', FORK_REMOTE, branch], checkout);
|
|
272
|
+
if (!pushed.ok) {
|
|
273
|
+
return { step: 'push-fork', ok: false, detail: failDetail('push-fork', pushed) };
|
|
274
|
+
}
|
|
275
|
+
prHead = `${login}:${branch}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const prArgs = ['pr', 'create', '--repo', repo, '--title', plan.title, '--body', plan.body];
|
|
279
|
+
if (prHead !== null) {
|
|
280
|
+
prArgs.push('--head', prHead);
|
|
281
|
+
}
|
|
282
|
+
const pr = deps.runner('gh', prArgs, checkout);
|
|
283
|
+
if (!pr.ok) {
|
|
284
|
+
return { step: 'pr-create', ok: false, detail: failDetail('pr-create', pr) };
|
|
285
|
+
}
|
|
286
|
+
process.stdout.write(`${pr.out}`);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The egress ladder, executed within ONE `release` call: direct push → fork
|
|
292
|
+
* push → issue → open-with-errors. Every rung is separately timed out; a rung
|
|
293
|
+
* that fails degrades to the next instead of aborting the release. When the
|
|
294
|
+
* whole ladder fails, the record stays `open` with every failed attempt
|
|
295
|
+
* attached (sanitized — command output can carry local paths).
|
|
296
|
+
*/
|
|
297
|
+
export function executeRelease(
|
|
298
|
+
record: DefectRecord,
|
|
299
|
+
plan: ReleasePlan,
|
|
300
|
+
probe: Probe,
|
|
301
|
+
repo: string,
|
|
302
|
+
deps: ReleaseDeps,
|
|
303
|
+
): ReleaseOutcome {
|
|
304
|
+
const attempts: EgressAttempt[] = [];
|
|
305
|
+
|
|
306
|
+
if (plan.route === 'pull-request') {
|
|
307
|
+
const checkout = probe.agentConfigCheckout!;
|
|
308
|
+
const branch = deps.runner('git', ['branch', '--show-current'], checkout).out.trim();
|
|
309
|
+
if (!branch || branch === 'main') {
|
|
310
|
+
// Precondition, not an egress failure: the authored fix exists and
|
|
311
|
+
// must ride its own branch — an issue instead would drop the fix.
|
|
312
|
+
attempts.push({
|
|
313
|
+
step: 'branch-check',
|
|
314
|
+
ok: false,
|
|
315
|
+
detail:
|
|
316
|
+
'the fix must sit on its own branch in the agent-config checkout ' +
|
|
317
|
+
`(currently: ${branch || 'detached'})`,
|
|
318
|
+
});
|
|
319
|
+
return { published: null, attempts };
|
|
320
|
+
}
|
|
321
|
+
const ladder: PushVia[] =
|
|
322
|
+
plan.pushVia === 'upstream' && probe.canFork
|
|
323
|
+
? ['upstream', 'fork']
|
|
324
|
+
: [plan.pushVia ?? 'upstream'];
|
|
325
|
+
for (const via of ladder) {
|
|
326
|
+
const failed = tryPullRequest(plan, probe, repo, branch, via, deps);
|
|
327
|
+
if (failed === null) {
|
|
328
|
+
return { published: 'pull-request', attempts };
|
|
329
|
+
}
|
|
330
|
+
attempts.push(failed);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (plan.route === 'pull-request' || plan.route === 'issue') {
|
|
335
|
+
// Re-render for the rung actually taken: a plan that degraded from
|
|
336
|
+
// pull-request must not file an issue whose body claims
|
|
337
|
+
// `Route: pull-request` (R2 finding #4).
|
|
338
|
+
const issueBody =
|
|
339
|
+
plan.route === 'issue' ? plan.body : renderReport(record, 'issue');
|
|
340
|
+
const issue = deps.runner('gh', [
|
|
341
|
+
'issue',
|
|
342
|
+
'create',
|
|
343
|
+
'--repo',
|
|
344
|
+
repo,
|
|
345
|
+
'--title',
|
|
346
|
+
plan.title,
|
|
347
|
+
'--body',
|
|
348
|
+
issueBody,
|
|
349
|
+
]);
|
|
350
|
+
if (issue.ok) {
|
|
351
|
+
process.stdout.write(`${issue.out}`);
|
|
352
|
+
return { published: 'issue', attempts };
|
|
353
|
+
}
|
|
354
|
+
attempts.push({ step: 'issue-create', ok: false, detail: failDetail('issue-create', issue) });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return { published: null, attempts };
|
|
107
358
|
}
|
|
108
359
|
|
|
109
360
|
function statusCmd(root: string): number {
|
|
@@ -142,7 +393,8 @@ function releaseCmd(root: string, argv: string[]): number {
|
|
|
142
393
|
const probe = probeMachine(process.env, process.cwd());
|
|
143
394
|
const plan = planRelease(record, probe, root);
|
|
144
395
|
|
|
145
|
-
|
|
396
|
+
const via = plan.pushVia !== null ? ` via=${plan.pushVia}` : '';
|
|
397
|
+
process.stdout.write(`self-repair:release ${fp} -> route=${plan.route}${via}\n`);
|
|
146
398
|
if (plan.blocked !== null) {
|
|
147
399
|
process.stdout.write(` WARN ${plan.blocked}\n`);
|
|
148
400
|
}
|
|
@@ -156,47 +408,25 @@ function releaseCmd(root: string, argv: string[]): number {
|
|
|
156
408
|
);
|
|
157
409
|
return EXIT_OK;
|
|
158
410
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!r.ok) {
|
|
172
|
-
return EXIT_FAIL;
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
const checkout = probe.agentConfigCheckout!;
|
|
176
|
-
const branch = run('git', ['branch', '--show-current'], checkout).out.trim();
|
|
177
|
-
if (!branch || branch === 'main') {
|
|
178
|
-
process.stderr.write(
|
|
179
|
-
' The fix must sit on its own branch in the agent-config checkout ' +
|
|
180
|
-
`(currently: ${branch || 'detached'}).\n`,
|
|
181
|
-
);
|
|
182
|
-
return EXIT_FAIL;
|
|
183
|
-
}
|
|
184
|
-
const pushed = run('git', ['push', '-u', 'origin', branch], checkout);
|
|
185
|
-
if (!pushed.ok) {
|
|
186
|
-
process.stderr.write(`${pushed.out}\n`);
|
|
187
|
-
return EXIT_FAIL;
|
|
188
|
-
}
|
|
189
|
-
const pr = run(
|
|
190
|
-
'gh',
|
|
191
|
-
['pr', 'create', '--repo', repo, '--title', plan.title, '--body', plan.body],
|
|
192
|
-
checkout,
|
|
411
|
+
|
|
412
|
+
const deps: ReleaseDeps = { runner: run, now: () => new Date().toISOString() };
|
|
413
|
+
const outcome = executeRelease(record, plan, probe, repo, deps);
|
|
414
|
+
|
|
415
|
+
for (const a of outcome.attempts) {
|
|
416
|
+
process.stderr.write(` FAIL ${a.detail}\n`);
|
|
417
|
+
}
|
|
418
|
+
if (outcome.published === null) {
|
|
419
|
+
const errors = outcome.attempts.map((a) => sanitizeEvidence(a.detail));
|
|
420
|
+
attachReleaseErrors(root, fp, errors, deps.now());
|
|
421
|
+
process.stderr.write(
|
|
422
|
+
' The egress ladder is exhausted; the record stays open with the errors attached.\n',
|
|
193
423
|
);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
424
|
+
return EXIT_FAIL;
|
|
425
|
+
}
|
|
426
|
+
if (outcome.published !== plan.route) {
|
|
427
|
+
process.stdout.write(` Degraded to ${outcome.published} after failed attempt(s).\n`);
|
|
198
428
|
}
|
|
199
|
-
markReleased(root, fp,
|
|
429
|
+
markReleased(root, fp, deps.now());
|
|
200
430
|
return EXIT_OK;
|
|
201
431
|
}
|
|
202
432
|
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Team-event payload shape spike (Phase 5.1, road-to-always-on-orchestration).
|
|
4
|
+
*
|
|
5
|
+
* `TaskCreated` / `TaskCompleted` / `TeammateIdle` are documented Claude Code
|
|
6
|
+
* hook events (agent teams, behind `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`),
|
|
7
|
+
* but this repo has never observed a real payload for any of them — the
|
|
8
|
+
* shapes are documented, not verified. This script is manually startable
|
|
9
|
+
* ONLY: it is never wired into `hook_manifest.yaml` / `hooks.json`. A
|
|
10
|
+
* maintainer points a hook command at it (or pipes a captured payload) to
|
|
11
|
+
* record a SHAPE — field names + JS/JSON types — and never a value.
|
|
12
|
+
* Privacy by construction: the recursive walk below inspects `typeof` /
|
|
13
|
+
* `Array.isArray`, nothing else, so there is no code path that could copy a
|
|
14
|
+
* value into the written report.
|
|
15
|
+
*
|
|
16
|
+
* SCOPE OF THE PRIVACY CLAIM (stated honestly, per independent review): the
|
|
17
|
+
* claim above holds for a payload whose OWN keys are static field names —
|
|
18
|
+
* true of the three documented envelopes this script targets
|
|
19
|
+
* (`TaskCreated` / `TaskCompleted` / `TeammateIdle`), where a key is always
|
|
20
|
+
* a schema name like `task_id` or `teammate_name`. It does NOT hold for a
|
|
21
|
+
* payload with dynamically-named keys — a map keyed by a file path, a
|
|
22
|
+
* person's name, or any other runtime value — because `extractShape` walks
|
|
23
|
+
* `Object.keys()` and records each key verbatim as a "field name". A
|
|
24
|
+
* dynamic key is not a field name; it is a value wearing a field name's
|
|
25
|
+
* clothes, and this script would write it into the tracked report exactly
|
|
26
|
+
* as it would refuse to write a value. Point this script only at the three
|
|
27
|
+
* documented shapes (or another envelope you have separately confirmed has
|
|
28
|
+
* static keys) — never at an undocumented payload that might carry a
|
|
29
|
+
* dynamically-keyed map.
|
|
30
|
+
*
|
|
31
|
+
* Usage (only meaningful with the experimental flag set):
|
|
32
|
+
*
|
|
33
|
+
* echo '<payload-json>' | \
|
|
34
|
+
* CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 npx tsx src/scripts/team_events_spike.ts [EventName]
|
|
35
|
+
*
|
|
36
|
+
* `EventName` is optional; if omitted, the payload's own `hook_event_name` /
|
|
37
|
+
* `event` / `eventName` / `event_name` / `type` field is read and matched
|
|
38
|
+
* against the three known `TEAM_EVENT_NAMES` — an unrecognised value falls
|
|
39
|
+
* back to `unlabeled-event` rather than copying the raw (payload-supplied)
|
|
40
|
+
* string into the report, which would itself be writing a value under the
|
|
41
|
+
* cover of a "name".
|
|
42
|
+
*
|
|
43
|
+
* Without the flag: clean one-line skip, exit 0, stdin is never touched.
|
|
44
|
+
*
|
|
45
|
+
* Output: appends one dated section per capture to
|
|
46
|
+
* `agents/evidence/analysis/team-event-shapes.md`.
|
|
47
|
+
*/
|
|
48
|
+
import * as fs from 'node:fs';
|
|
49
|
+
import * as path from 'node:path';
|
|
50
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
51
|
+
|
|
52
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
53
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
54
|
+
const OUT_FILE = path.join(ROOT, 'agents', 'evidence', 'analysis', 'team-event-shapes.md');
|
|
55
|
+
|
|
56
|
+
export const FLAG = 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS';
|
|
57
|
+
export const TEAM_EVENT_NAMES = ['TaskCreated', 'TaskCompleted', 'TeammateIdle'] as const;
|
|
58
|
+
|
|
59
|
+
export interface ShapeEntry {
|
|
60
|
+
path: string;
|
|
61
|
+
type: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** JS/JSON type of a value. Reads only `typeof` / `Array.isArray` — never the value. */
|
|
65
|
+
function _typeOf(value: unknown): string {
|
|
66
|
+
if (value === null) return 'null';
|
|
67
|
+
if (Array.isArray(value)) return 'array';
|
|
68
|
+
return typeof value; // 'string' | 'number' | 'boolean' | 'object' | 'undefined'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Walk a parsed JSON payload and record field-name + type only.
|
|
73
|
+
*
|
|
74
|
+
* This never returns, prints, or embeds a value — recursion is driven
|
|
75
|
+
* entirely by structural type and object keys.
|
|
76
|
+
*/
|
|
77
|
+
export function extractShape(value: unknown, prefix = ''): ShapeEntry[] {
|
|
78
|
+
const entries: ShapeEntry[] = [];
|
|
79
|
+
const t = _typeOf(value);
|
|
80
|
+
if (prefix !== '') {
|
|
81
|
+
entries.push({ path: prefix, type: t });
|
|
82
|
+
}
|
|
83
|
+
if (t === 'array') {
|
|
84
|
+
// An event payload's arrays are homogeneous lists, not fixed tuples —
|
|
85
|
+
// merge every element's shape under one `[]` path (deduped below).
|
|
86
|
+
for (const item of value as unknown[]) {
|
|
87
|
+
entries.push(...extractShape(item, `${prefix}[]`));
|
|
88
|
+
}
|
|
89
|
+
return entries;
|
|
90
|
+
}
|
|
91
|
+
if (t === 'object') {
|
|
92
|
+
const obj = value as Record<string, unknown>;
|
|
93
|
+
for (const key of Object.keys(obj).sort()) {
|
|
94
|
+
entries.push(...extractShape(obj[key], prefix === '' ? key : `${prefix}.${key}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return entries;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** De-duplicate by path+type — an array's elements repeat the same shape. */
|
|
101
|
+
function _dedupe(entries: ShapeEntry[]): ShapeEntry[] {
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
const out: ShapeEntry[] = [];
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
const key = `${e.path}\u0000${e.type}`;
|
|
106
|
+
if (seen.has(key)) continue;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
out.push(e);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Best-effort event-name inference from the payload's own envelope.
|
|
115
|
+
*
|
|
116
|
+
* m4 fix (independent-review finding): the envelope field this reads
|
|
117
|
+
* (`hook_event_name` / `event` / `type` / …) carries a payload-SUPPLIED
|
|
118
|
+
* string — copying it verbatim into the recorded report is exactly the
|
|
119
|
+
* "copy a value into the report" this script otherwise refuses to do
|
|
120
|
+
* (see the module header's privacy claim). Matched against the known
|
|
121
|
+
* `TEAM_EVENT_NAMES` allowlist instead: a recognised name is safe to
|
|
122
|
+
* record because it is drawn from three publicly documented, stable
|
|
123
|
+
* literals, never from the untrusted payload string itself; anything
|
|
124
|
+
* else returns `null` and the caller falls back to the fixed literal
|
|
125
|
+
* `'unlabeled-event'`.
|
|
126
|
+
*/
|
|
127
|
+
export function inferEventName(payload: unknown): string | null {
|
|
128
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
129
|
+
const obj = payload as Record<string, unknown>;
|
|
130
|
+
const known = new Set<string>(TEAM_EVENT_NAMES);
|
|
131
|
+
for (const key of ['hook_event_name', 'event', 'eventName', 'event_name', 'type']) {
|
|
132
|
+
const v = obj[key];
|
|
133
|
+
if (typeof v === 'string' && known.has(v)) return v;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Render one capture as a Markdown section — field names + types only. */
|
|
139
|
+
export function renderShapeReport(eventName: string, entries: ShapeEntry[], observedAt: string): string {
|
|
140
|
+
const deduped = _dedupe(entries);
|
|
141
|
+
const rows = deduped.map((e) => `| \`${e.path}\` | ${e.type} |`).join('\n');
|
|
142
|
+
const table = rows === '' ? '_(empty payload — no fields observed)_' : `| Field | Type |\n|---|---|\n${rows}`;
|
|
143
|
+
return [`## ${eventName} — observed ${observedAt}`, '', table, ''].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _printGuide(): void {
|
|
147
|
+
process.stdout.write(
|
|
148
|
+
[
|
|
149
|
+
'team-event shape spike (manual, not hook-bound)',
|
|
150
|
+
'',
|
|
151
|
+
`${FLAG} is set — recording.`,
|
|
152
|
+
'',
|
|
153
|
+
`Observed events: ${TEAM_EVENT_NAMES.join(', ')}`,
|
|
154
|
+
'',
|
|
155
|
+
'Pipe one captured JSON payload on stdin — values are never written,',
|
|
156
|
+
'only field names + types:',
|
|
157
|
+
'',
|
|
158
|
+
` echo '<payload-json>' | ${FLAG}=1 npx tsx src/scripts/team_events_spike.ts [EventName]`,
|
|
159
|
+
'',
|
|
160
|
+
`Shapes append to ${path.relative(ROOT, OUT_FILE)}.`,
|
|
161
|
+
'',
|
|
162
|
+
].join('\n'),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function _readStdin(): Promise<string> {
|
|
167
|
+
const chunks: Buffer[] = [];
|
|
168
|
+
// `for await` streams stdin — a synchronous `readFileSync(0)` EAGAINs
|
|
169
|
+
// past the pipe buffer on a large payload; this has no such ceiling.
|
|
170
|
+
for await (const chunk of process.stdin) {
|
|
171
|
+
chunks.push(chunk as Buffer);
|
|
172
|
+
}
|
|
173
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface RunResult {
|
|
177
|
+
skipped: boolean;
|
|
178
|
+
reason?: string;
|
|
179
|
+
eventName?: string;
|
|
180
|
+
entryCount?: number;
|
|
181
|
+
outFile?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RunOptions {
|
|
185
|
+
argv?: string[];
|
|
186
|
+
env?: NodeJS.ProcessEnv;
|
|
187
|
+
readStdin?: () => Promise<string>;
|
|
188
|
+
writeReport?: (section: string) => void;
|
|
189
|
+
now?: () => string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Orchestrates the spike. Every side effect is injectable so tests never
|
|
194
|
+
* touch the real environment, stdin, or the tracked report file.
|
|
195
|
+
*/
|
|
196
|
+
export async function run(opts?: RunOptions): Promise<RunResult> {
|
|
197
|
+
const env = opts?.env ?? process.env;
|
|
198
|
+
const argv = opts?.argv ?? process.argv.slice(2);
|
|
199
|
+
const readStdin = opts?.readStdin ?? _readStdin;
|
|
200
|
+
const now = opts?.now ?? (() => new Date().toISOString());
|
|
201
|
+
const writeReport =
|
|
202
|
+
opts?.writeReport ??
|
|
203
|
+
((section: string) => {
|
|
204
|
+
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
|
205
|
+
fs.appendFileSync(OUT_FILE, section, 'utf-8');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!env[FLAG]) {
|
|
209
|
+
const reason = `${FLAG} is not set — teams are experimental and unavailable; skipping.`;
|
|
210
|
+
process.stdout.write(`${reason}\n`);
|
|
211
|
+
return { skipped: true, reason };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_printGuide();
|
|
215
|
+
const raw = (await readStdin()).trim();
|
|
216
|
+
if (raw === '') {
|
|
217
|
+
const reason = 'no payload received on stdin — nothing recorded.';
|
|
218
|
+
process.stdout.write(`${reason}\n`);
|
|
219
|
+
return { skipped: true, reason };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let payload: unknown;
|
|
223
|
+
try {
|
|
224
|
+
payload = JSON.parse(raw);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const reason = `invalid JSON on stdin: ${(e as Error).message}`;
|
|
227
|
+
process.stderr.write(`team_events_spike: ${reason}\n`);
|
|
228
|
+
return { skipped: true, reason };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const eventName = argv[0] ?? inferEventName(payload) ?? 'unlabeled-event';
|
|
232
|
+
const entries = extractShape(payload);
|
|
233
|
+
const section = renderShapeReport(eventName, entries, now());
|
|
234
|
+
writeReport(`\n${section}`);
|
|
235
|
+
const count = _dedupe(entries).length;
|
|
236
|
+
process.stdout.write(`recorded ${count} field shape(s) for ${eventName} -> ${path.relative(ROOT, OUT_FILE)}\n`);
|
|
237
|
+
return { skipped: false, eventName, entryCount: count, outFile: OUT_FILE };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _isCliEntry(): boolean {
|
|
241
|
+
if (process.argv[1] === undefined) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
245
|
+
if (import.meta.url === argvUrl) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
// A symlinked invocation (e.g. via an installed `.augment/` projection)
|
|
249
|
+
// makes the raw URLs differ; compare realpaths so the entry guard still
|
|
250
|
+
// fires when run through a symlink.
|
|
251
|
+
try {
|
|
252
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
253
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
254
|
+
return here === argv;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
261
|
+
run()
|
|
262
|
+
.then(() => {
|
|
263
|
+
// Observational spike, never a gate — always exit 0 on a
|
|
264
|
+
// completed run (skip or record alike).
|
|
265
|
+
process.exit(0);
|
|
266
|
+
})
|
|
267
|
+
.catch((e: unknown) => {
|
|
268
|
+
process.stderr.write(`team_events_spike: ${(e as Error).message}\n`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
}
|