@event4u/agent-config 9.16.0 → 9.17.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 +1 -1
- package/CHANGELOG.md +53 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/architecture.md +2 -2
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +1 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3194 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- 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 +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +31 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +602 -507
- package/dist/install/install.mjs +23 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +13 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +22 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +30 -0
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +9 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +325 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +23 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -321,6 +321,28 @@ personal:
|
|
|
321
321
|
# See user-types/README.md for the per-id contract.
|
|
322
322
|
user_type: "__USER_TYPE__"
|
|
323
323
|
|
|
324
|
+
# --- Reply verbosity ---
|
|
325
|
+
verbosity:
|
|
326
|
+
# Announce intent before tool batches ("Let me check X…") — only honored
|
|
327
|
+
# when personal.play_by_play is ALSO true (the direct-answers narration
|
|
328
|
+
# carve-out requires both). Default false = no intent narration.
|
|
329
|
+
intent_announcements: false
|
|
330
|
+
|
|
331
|
+
# Show generated commit messages, PR titles/bodies, branch names before
|
|
332
|
+
# acting. false = use generated content directly (/commit terse path).
|
|
333
|
+
preview_artifacts: false
|
|
334
|
+
|
|
335
|
+
# Confirmation prompts for routine workflow steps with one obvious answer.
|
|
336
|
+
# Iron-Law gates (commit-policy, scope-control git-ops, Hard Floor) ALWAYS
|
|
337
|
+
# ask regardless of this setting.
|
|
338
|
+
routine_confirmations: false
|
|
339
|
+
|
|
340
|
+
# Offer "run AI Council on this?" inside delivery commands.
|
|
341
|
+
offer_council_in_delivery: false
|
|
342
|
+
|
|
343
|
+
# Status blocks after a successful action: off | minimal | full.
|
|
344
|
+
post_action_reports: minimal
|
|
345
|
+
|
|
324
346
|
# --- Project / team preferences ---
|
|
325
347
|
project:
|
|
326
348
|
# Path to the PR template file (relative to project root)
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"on_release": "fail",
|
|
9
9
|
"on_main_and_prs": "warn",
|
|
10
10
|
"on_local": "fail",
|
|
11
|
-
"always_blocking": [
|
|
11
|
+
"always_blocking": [
|
|
12
|
+
"a metric with no measurement supplied"
|
|
13
|
+
],
|
|
12
14
|
"rationale": "road-to-gates-that-can-fail Phase 5. A hard fail on main turns every legitimate command addition into a blocked merge, which is how budgets get quietly raised with a cushion instead of consciously — so budget / regression / drift findings are ADVISORY on main, the nightly and ordinary PRs, and BLOCKING on a release ref. Not a weakening: before the Phase-4 path triggers, this gate ran on release PRs and the nightly only, so the surfaces that now warn never saw it at all. A LOCAL run (no CI env) is strict, because a maintainer running it by hand — including release.ts — wants the strict answer and blocks nobody. A metric with NO measurement is always blocking in either posture: that is a broken harness reporting green on nothing. Implemented in src/scripts/check_evaluator_budgets.ts::detectPosture; override with --posture warn|fail.",
|
|
13
15
|
"delivery": "This posture IS the nightly-visibility route (council 2026-08-02, blocker nightly-visibility-owner), not a second mechanism beside it: the nightly publishes measurement_record below, a later run re-measures and compares, and the contradiction warns on main / fails on release. No notification channel, no auto-managed issue — a one-maintainer channel producing routine noise gets muted, and a muted channel reproduces the original failure."
|
|
14
16
|
},
|
|
@@ -25,13 +27,8 @@
|
|
|
25
27
|
},
|
|
26
28
|
"not_re_measured": "unpacked_size_mb, node_modules_mb, runtime_dep_count, cli_version_cold_ms, mcp_boot_to_initialize_ms. All five require the packed-tarball harness (npm pack of a fully built tree, then a clean `npm install --omit=dev <tarball>` into a scratch project) that evaluator_umbrella.sh runs in CI. They were NOT re-measured in this pass and their values remain the 2026-07-27 acceptance numbers — stated rather than invented. The next nightly writes the real numbers into measurement_record, and the drift check then reports any contradiction with last_measured on the PR that carries it."
|
|
27
29
|
},
|
|
30
|
+
"removed_2026_08_04": "unpacked_size_mb (max 28) — removed by maintainer decision at the 9.17.0 release: the gate turned every legitimate payload addition into a blocked release (measured 28.22 vs max 28 with all functional gates green), and the maintainer judged tarball size not worth gating. evaluator_umbrella.sh still MEASURES and records unpacked_size_mb as evidence; only the budget enforcement is gone. ADR-204 review_trigger (a) fired and was resolved this way.",
|
|
28
31
|
"budgets": {
|
|
29
|
-
"unpacked_size_mb": {
|
|
30
|
-
"max": 28,
|
|
31
|
-
"last_measured": 26.05,
|
|
32
|
-
"last_measured_at": "2026-07-27",
|
|
33
|
-
"method": "npm pack --json → dist.unpackedSize / 1e6 — includes the Phase-1 precompiled bundles (dist/hooks/dispatch.js + dist/mcp/server.mjs, ~2.1 MB) that bought the 23x hook-latency win and the tsx removal, plus dist/cli-delegate/ (~1.40 MB, ADR-204) which finished that removal for the consumer command surface. This gate is the ONLY size trip-wire on those bundles by design: a dedicated per-bundle budget key would need its own measurement wired into evaluator_umbrella.sh, and an unwired key fails check_evaluator_budgets with 'no measurement supplied' instead of guarding anything. ADR-204 projected 27.45 against max 28 without packing — the next real measurement here is the confirming one, and headroom is thin"
|
|
34
|
-
},
|
|
35
32
|
"node_modules_mb": {
|
|
36
33
|
"max": 140,
|
|
37
34
|
"last_measured": 127,
|
|
@@ -61,11 +58,11 @@
|
|
|
61
58
|
"deterministic": false
|
|
62
59
|
},
|
|
63
60
|
"cli_help_command_count": {
|
|
64
|
-
"max":
|
|
65
|
-
"last_measured":
|
|
66
|
-
"last_measured_at": "2026-08-
|
|
61
|
+
"max": 82,
|
|
62
|
+
"last_measured": 82,
|
|
63
|
+
"last_measured_at": "2026-08-04",
|
|
67
64
|
"method": "registry enumeration — count of `{ name: '...', disposition` entries in src/cli/registry.ts (NOT --help prose parsing; methods historically disagreed 74 vs 76 vs 79)",
|
|
68
|
-
"baseline_note": "80 → 81 on 2026-08-03 (release 9.15.0 gate). This RECORDS growth that already shipped through its own reviewed PR; it does not authorize new surface. The +1 is f02db54c2 (fix(cli): register routing:doctor in the CLI registry) — the command existed, only its registry entry was missing, and the registering PR did not move this budget. The gate did its job again: breach WARNS on main and BLOCKS on release, which is the conscious-decision point rather than a silently raised cap. Re-measured 2026-08-03 via the pinned method: 81. Headroom stays zero by design — the next registry addition must move this number in its own PR."
|
|
65
|
+
"baseline_note": "80 → 81 on 2026-08-03 (release 9.15.0 gate). This RECORDS growth that already shipped through its own reviewed PR; it does not authorize new surface. The +1 is f02db54c2 (fix(cli): register routing:doctor in the CLI registry) — the command existed, only its registry entry was missing, and the registering PR did not move this budget. The gate did its job again: breach WARNS on main and BLOCKS on release, which is the conscious-decision point rather than a silently raised cap. Re-measured 2026-08-03 via the pinned method: 81. Headroom stays zero by design — the next registry addition must move this number in its own PR. 81 → 82 on 2026-08-04: the `handoff` subcommand (road-to-agent-handoff-resume Phase 4) registers in the same PR that adds the surface, per this record's own contract. Re-measured via the pinned method: 82."
|
|
69
66
|
},
|
|
70
67
|
"mcp_public_tool_count": {
|
|
71
68
|
"max": 19,
|
|
@@ -260,6 +260,36 @@ gates:
|
|
|
260
260
|
failure direction was false-positive rather than false-green, but the
|
|
261
261
|
cause was the same dead root.
|
|
262
262
|
|
|
263
|
+
- id: lint_abstraction_thresholds
|
|
264
|
+
argv: []
|
|
265
|
+
min_scanned: 400
|
|
266
|
+
corpus: "src/rules + src/skills + src/agent-src/commands + docs/guidelines .md (533 at baseline)"
|
|
267
|
+
status: enforced
|
|
268
|
+
note: >
|
|
269
|
+
A collapse below the floor would mean the gate stopped reading the tree
|
|
270
|
+
that states extraction thresholds, so a bare or diverging threshold
|
|
271
|
+
(ADR-213 drift) could land while the gate still printed green.
|
|
272
|
+
|
|
273
|
+
- id: lint_profile_personas
|
|
274
|
+
argv: []
|
|
275
|
+
min_scanned: 5
|
|
276
|
+
corpus: "src/agent-src/profiles/*.yml (6 seed profiles at baseline)"
|
|
277
|
+
status: enforced
|
|
278
|
+
note: >
|
|
279
|
+
Fewer than 5 profiles scanned would mean the gate stopped reading the
|
|
280
|
+
seed-profile dir, so a defaults.personas id that resolves to no persona
|
|
281
|
+
file could land while the gate still printed green.
|
|
282
|
+
|
|
283
|
+
- id: lint_token_budget_discipline
|
|
284
|
+
argv: []
|
|
285
|
+
min_scanned: 200
|
|
286
|
+
corpus: "src/skills/*/SKILL.md (288 at baseline)"
|
|
287
|
+
status: enforced
|
|
288
|
+
note: >
|
|
289
|
+
A collapse below the floor would mean the gate stopped reading the skills
|
|
290
|
+
tree, so a rich-tagged skill without its justification section (or a cap
|
|
291
|
+
breach) could land while the gate still printed green.
|
|
292
|
+
|
|
263
293
|
- id: lint_framework_leakage
|
|
264
294
|
argv: []
|
|
265
295
|
min_scanned: 350
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `agent-config handoff` — pick a recent session, generate a handoff,
|
|
4
|
+
* seed a fresh session (road-to-agent-handoff-resume Phase 4).
|
|
5
|
+
*
|
|
6
|
+
* Default TTY flow: numbered picker over the unified session list
|
|
7
|
+
* (chat-history primary; Claude-transcript + Codex-session adapters),
|
|
8
|
+
* then the Phase-2 deterministic generator writes
|
|
9
|
+
* `agents/runtime/state/handoff-context.md`, which the Phase-3
|
|
10
|
+
* session_start hook injects into the NEXT session exactly once.
|
|
11
|
+
*
|
|
12
|
+
* Flags:
|
|
13
|
+
* --list print the sessions and exit
|
|
14
|
+
* --json with --list: machine-readable output
|
|
15
|
+
* --session <id> skip the picker (exact id or unique prefix)
|
|
16
|
+
* --print emit the handoff to stdout instead of the state
|
|
17
|
+
* file (legacy copy-paste mode)
|
|
18
|
+
* --launch <host> spawn a fresh interactive host session after
|
|
19
|
+
* generating (claude: hook-seeded; codex: handoff as
|
|
20
|
+
* initial prompt — no hook surface there)
|
|
21
|
+
* --llm narrative-polish seam; v1: clear not-implemented error
|
|
22
|
+
* --root <path> repo root override (default: cwd)
|
|
23
|
+
*
|
|
24
|
+
* Exit codes: 0 ok · 1 no sessions / session not found · 2 invocation error
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import process from 'node:process';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
build_handoff,
|
|
35
|
+
handoff_target_path,
|
|
36
|
+
LlmPolishNotImplementedError,
|
|
37
|
+
write_handoff,
|
|
38
|
+
} from './handoff_generate.js';
|
|
39
|
+
import { list_handoff_sessions } from './handoff_sessions.js';
|
|
40
|
+
import type { HandoffSession } from './handoff_sessions.js';
|
|
41
|
+
|
|
42
|
+
const PROG = 'agent-config handoff';
|
|
43
|
+
const USAGE = `usage: ${PROG} [--list] [--json] [--session <id>] [--print] [--launch <host>] [--llm] [--root <path>]`;
|
|
44
|
+
|
|
45
|
+
const _HELP = `${USAGE}
|
|
46
|
+
|
|
47
|
+
Pick a recent session, generate a deterministic handoff, and seed a fresh
|
|
48
|
+
session with it. Without flags (in a TTY): numbered picker, then the handoff
|
|
49
|
+
lands in agents/runtime/state/handoff-context.md and auto-injects into the
|
|
50
|
+
next session on hook-capable hosts.
|
|
51
|
+
|
|
52
|
+
options:
|
|
53
|
+
--list print the recent sessions and exit
|
|
54
|
+
--json with --list: JSON output
|
|
55
|
+
--session <id> generate for this session id (exact or unique prefix)
|
|
56
|
+
--print print the handoff to stdout (no state file)
|
|
57
|
+
--launch <host> start a fresh interactive session (claude | codex)
|
|
58
|
+
--llm optional LLM polish — not implemented in v1
|
|
59
|
+
--root <path> repo root (default: current directory)
|
|
60
|
+
-h, --help show this help
|
|
61
|
+
|
|
62
|
+
exit codes:
|
|
63
|
+
0 handoff generated (or list printed)
|
|
64
|
+
1 no sessions found / session id not found
|
|
65
|
+
2 invocation error (bad flag, non-TTY without --session, unsupported host)
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
class ArgparseExit extends Error {
|
|
69
|
+
constructor(public readonly code: number) {
|
|
70
|
+
super(`exit ${code}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------
|
|
75
|
+
// launch adapter table (v1: claude + codex live, gemini stubbed)
|
|
76
|
+
// ---------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export interface LaunchAdapter {
|
|
79
|
+
supports_launch: boolean;
|
|
80
|
+
/** 'hook' = state file + session_start injection; 'initial-prompt' = argv. */
|
|
81
|
+
seeding: 'hook' | 'initial-prompt';
|
|
82
|
+
build_argv: (handoffText: string) => string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const LAUNCH_ADAPTERS: Record<string, LaunchAdapter> = {
|
|
86
|
+
claude: {
|
|
87
|
+
supports_launch: true,
|
|
88
|
+
seeding: 'hook',
|
|
89
|
+
build_argv: () => ['claude'],
|
|
90
|
+
},
|
|
91
|
+
codex: {
|
|
92
|
+
// Codex is a bundle host without hooks — seed via the initial prompt.
|
|
93
|
+
supports_launch: true,
|
|
94
|
+
seeding: 'initial-prompt',
|
|
95
|
+
build_argv: (handoffText: string) => ['codex', handoffText],
|
|
96
|
+
},
|
|
97
|
+
gemini: {
|
|
98
|
+
supports_launch: false,
|
|
99
|
+
seeding: 'hook',
|
|
100
|
+
build_argv: () => ['gemini'],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------
|
|
105
|
+
// options + seams
|
|
106
|
+
// ---------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export interface Opts {
|
|
109
|
+
list: boolean;
|
|
110
|
+
json: boolean;
|
|
111
|
+
session: string | null;
|
|
112
|
+
print: boolean;
|
|
113
|
+
launch: string | null;
|
|
114
|
+
llm: boolean;
|
|
115
|
+
root: string | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type Runner = (cmd: string[]) => number;
|
|
119
|
+
|
|
120
|
+
export interface MainOptions {
|
|
121
|
+
out?: (text: string) => void;
|
|
122
|
+
err?: (text: string) => void;
|
|
123
|
+
/** TTY availability override (default: stdin AND stdout are TTYs). */
|
|
124
|
+
isTTY?: boolean;
|
|
125
|
+
/** Picker input; returns the raw line or null on read failure. */
|
|
126
|
+
readLine?: (prompt: string) => string | null;
|
|
127
|
+
/** Interactive child spawn (default: spawnSync stdio-inherit). */
|
|
128
|
+
runner?: Runner;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _default_runner(cmd: string[]): number {
|
|
132
|
+
const r = spawnSync(cmd[0] as string, cmd.slice(1), { stdio: 'inherit' });
|
|
133
|
+
if (r.error) {
|
|
134
|
+
process.stderr.write(`${PROG}: cannot run ${cmd[0]}: ${String(r.error)}\n`);
|
|
135
|
+
return 127;
|
|
136
|
+
}
|
|
137
|
+
return r.status ?? 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Synchronous /dev/tty line read (never fd 0 — EAGAIN under go-task
|
|
142
|
+
* `interactive: true` silently auto-aborts a fd-0 prompt).
|
|
143
|
+
*/
|
|
144
|
+
function _tty_read_line(prompt: string): string | null {
|
|
145
|
+
try {
|
|
146
|
+
process.stdout.write(prompt);
|
|
147
|
+
const fd = fs.openSync('/dev/tty', 'rs');
|
|
148
|
+
try {
|
|
149
|
+
const buf = Buffer.alloc(1);
|
|
150
|
+
let line = '';
|
|
151
|
+
for (;;) {
|
|
152
|
+
let n = 0;
|
|
153
|
+
try {
|
|
154
|
+
n = fs.readSync(fd, buf, 0, 1, null);
|
|
155
|
+
} catch (exc) {
|
|
156
|
+
const code = (exc as NodeJS.ErrnoException).code;
|
|
157
|
+
if (code === 'EAGAIN' || code === 'EINTR') continue;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (n === 0) break;
|
|
161
|
+
const ch = buf.toString('utf-8');
|
|
162
|
+
if (ch === '\n') break;
|
|
163
|
+
line += ch;
|
|
164
|
+
}
|
|
165
|
+
return line;
|
|
166
|
+
} finally {
|
|
167
|
+
fs.closeSync(fd);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------
|
|
175
|
+
// arg parsing (hand-rolled, house style — no commander in _cli/)
|
|
176
|
+
// ---------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function _argError(err: (t: string) => void, message: string): never {
|
|
179
|
+
err(`${USAGE}\n${PROG}: error: ${message}\n`);
|
|
180
|
+
throw new ArgparseExit(2);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function _parse(argv: string[], err: (t: string) => void): Opts {
|
|
184
|
+
const opts: Opts = {
|
|
185
|
+
list: false,
|
|
186
|
+
json: false,
|
|
187
|
+
session: null,
|
|
188
|
+
print: false,
|
|
189
|
+
launch: null,
|
|
190
|
+
llm: false,
|
|
191
|
+
root: null,
|
|
192
|
+
};
|
|
193
|
+
let i = 0;
|
|
194
|
+
const takeValue = (flag: string): string => {
|
|
195
|
+
const arg = argv[i] as string;
|
|
196
|
+
const eq = arg.indexOf('=');
|
|
197
|
+
if (eq >= 0) return arg.slice(eq + 1);
|
|
198
|
+
i += 1;
|
|
199
|
+
const value = argv[i];
|
|
200
|
+
if (value === undefined) _argError(err, `argument ${flag}: expected one argument`);
|
|
201
|
+
return value;
|
|
202
|
+
};
|
|
203
|
+
while (i < argv.length) {
|
|
204
|
+
const arg = argv[i] as string;
|
|
205
|
+
const flag = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
206
|
+
switch (flag) {
|
|
207
|
+
case '-h':
|
|
208
|
+
case '--help':
|
|
209
|
+
process.stdout.write(_HELP);
|
|
210
|
+
throw new ArgparseExit(0);
|
|
211
|
+
case '--list':
|
|
212
|
+
opts.list = true;
|
|
213
|
+
break;
|
|
214
|
+
case '--json':
|
|
215
|
+
opts.json = true;
|
|
216
|
+
break;
|
|
217
|
+
case '--print':
|
|
218
|
+
opts.print = true;
|
|
219
|
+
break;
|
|
220
|
+
case '--llm':
|
|
221
|
+
opts.llm = true;
|
|
222
|
+
break;
|
|
223
|
+
case '--session':
|
|
224
|
+
opts.session = takeValue('--session');
|
|
225
|
+
break;
|
|
226
|
+
case '--launch':
|
|
227
|
+
opts.launch = takeValue('--launch');
|
|
228
|
+
break;
|
|
229
|
+
case '--root':
|
|
230
|
+
opts.root = takeValue('--root');
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
_argError(err, `unrecognized arguments: ${arg}`);
|
|
234
|
+
}
|
|
235
|
+
i += 1;
|
|
236
|
+
}
|
|
237
|
+
return opts;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------
|
|
241
|
+
// rendering
|
|
242
|
+
// ---------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
function _fmt_date(iso: string | null): string {
|
|
245
|
+
if (!iso) return 'unknown-date';
|
|
246
|
+
return iso.slice(0, 16).replace('T', ' ');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function render_session_line(s: HandoffSession, index: number): string {
|
|
250
|
+
const branch = s.branch ? ` · ${s.branch}` : '';
|
|
251
|
+
return `${String(index + 1).padStart(2)}. ${_fmt_date(s.endedAt ?? s.startedAt)} · ${s.source}${branch} · ${s.summary}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _render_list(sessions: HandoffSession[], json: boolean): string {
|
|
255
|
+
if (json) return JSON.stringify(sessions, null, 2) + '\n';
|
|
256
|
+
return sessions.map((s, i) => render_session_line(s, i)).join('\n') + '\n';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------
|
|
260
|
+
// main
|
|
261
|
+
// ---------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
export function main(argv: string[] | null = null, options: MainOptions = {}): number {
|
|
264
|
+
const out = options.out ?? ((t: string): void => void process.stdout.write(t));
|
|
265
|
+
const err = options.err ?? ((t: string): void => void process.stderr.write(t));
|
|
266
|
+
const isTTY = options.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
267
|
+
const readLine = options.readLine ?? _tty_read_line;
|
|
268
|
+
const runner = options.runner ?? _default_runner;
|
|
269
|
+
|
|
270
|
+
let opts: Opts;
|
|
271
|
+
try {
|
|
272
|
+
opts = _parse(argv ?? process.argv.slice(2), err);
|
|
273
|
+
} catch (exc) {
|
|
274
|
+
if (exc instanceof ArgparseExit) return exc.code;
|
|
275
|
+
throw exc;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The bash dispatcher strips a global `--root` and exports it as
|
|
279
|
+
// AGENT_CONFIG_PROJECT_ROOT instead — honor both channels.
|
|
280
|
+
const envRoot =
|
|
281
|
+
process.env.AGENT_CONFIG_ROOT_OVERRIDE === '1'
|
|
282
|
+
? process.env.AGENT_CONFIG_PROJECT_ROOT || null
|
|
283
|
+
: null;
|
|
284
|
+
const root = path.resolve(opts.root ?? envRoot ?? process.cwd());
|
|
285
|
+
const sessions = list_handoff_sessions({ cwd: root });
|
|
286
|
+
|
|
287
|
+
if (opts.list) {
|
|
288
|
+
if (sessions.length === 0) {
|
|
289
|
+
out('no recent sessions found for this repo\n');
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
out(_render_list(sessions, opts.json));
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (sessions.length === 0) {
|
|
297
|
+
err(`${PROG}: no recent sessions found for this repo\n`);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (opts.launch && !(opts.launch in LAUNCH_ADAPTERS)) {
|
|
302
|
+
err(`${USAGE}\n${PROG}: error: unknown --launch host '${opts.launch}' (claude | codex)\n`);
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
const adapter = opts.launch ? (LAUNCH_ADAPTERS[opts.launch] as LaunchAdapter) : null;
|
|
306
|
+
if (opts.launch && adapter && !adapter.supports_launch) {
|
|
307
|
+
err(`${PROG}: --launch ${opts.launch} is not supported yet (hook injection already works there)\n`);
|
|
308
|
+
return 2;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// resolve the session — flag, or picker in a TTY
|
|
312
|
+
let picked: HandoffSession | undefined;
|
|
313
|
+
if (opts.session) {
|
|
314
|
+
const matches = sessions.filter((s) => s.id === opts.session || s.id.startsWith(opts.session as string));
|
|
315
|
+
if (matches.length === 0) {
|
|
316
|
+
err(`${PROG}: session '${opts.session}' not found — run --list\n`);
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
if (matches.length > 1) {
|
|
320
|
+
err(`${PROG}: session prefix '${opts.session}' is ambiguous (${matches.length} matches)\n`);
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
picked = matches[0];
|
|
324
|
+
} else if (isTTY) {
|
|
325
|
+
out('Recent sessions:\n');
|
|
326
|
+
out(_render_list(sessions, false));
|
|
327
|
+
const raw = readLine(`Pick a session [1-${sessions.length}]: `);
|
|
328
|
+
const n = Number((raw ?? '').trim());
|
|
329
|
+
if (!Number.isInteger(n) || n < 1 || n > sessions.length) {
|
|
330
|
+
err(`${PROG}: invalid selection\n`);
|
|
331
|
+
return 2;
|
|
332
|
+
}
|
|
333
|
+
picked = sessions[n - 1];
|
|
334
|
+
} else {
|
|
335
|
+
out(_render_list(sessions, false));
|
|
336
|
+
err(`${PROG}: non-interactive invocation needs --session <id> (list above)\n`);
|
|
337
|
+
return 2;
|
|
338
|
+
}
|
|
339
|
+
if (!picked) return 2;
|
|
340
|
+
|
|
341
|
+
let text: string;
|
|
342
|
+
try {
|
|
343
|
+
text = build_handoff(picked, { cwd: root, llm: opts.llm });
|
|
344
|
+
} catch (exc) {
|
|
345
|
+
if (exc instanceof LlmPolishNotImplementedError) {
|
|
346
|
+
err(`${PROG}: ${exc.message}\n`);
|
|
347
|
+
return 2;
|
|
348
|
+
}
|
|
349
|
+
throw exc;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (opts.print) {
|
|
353
|
+
out(text);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (adapter && adapter.seeding === 'initial-prompt') {
|
|
358
|
+
// no state file on this path — nothing would consume it
|
|
359
|
+
out(`launching ${opts.launch} with the handoff as initial prompt…\n`);
|
|
360
|
+
return runner(adapter.build_argv(text));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Only the most recent handoff persists — a second `handoff` run before
|
|
364
|
+
// the next session start replaces the unconsumed file (by design; the
|
|
365
|
+
// hook consumes exactly one).
|
|
366
|
+
const existed = fs.existsSync(handoff_target_path(root));
|
|
367
|
+
const target = write_handoff(text, { cwd: root });
|
|
368
|
+
if (existed) out('note: replaced an unconsumed handoff — only the most recent one persists.\n');
|
|
369
|
+
out(`handoff written: ${target}\n`);
|
|
370
|
+
out('start a new session — the handoff will be injected automatically (hook-capable hosts).\n');
|
|
371
|
+
|
|
372
|
+
if (adapter) {
|
|
373
|
+
out(`launching ${opts.launch}…\n`);
|
|
374
|
+
return runner(adapter.build_argv(text));
|
|
375
|
+
}
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------
|
|
380
|
+
// entry guard (symlink-safe; same shape as cmd_analyze_session.ts —
|
|
381
|
+
// under the cli-delegate bundle the argv-URL compare matches the bundle)
|
|
382
|
+
// ---------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
function _isCliEntry(): boolean {
|
|
385
|
+
if (process.argv[1] === undefined) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
389
|
+
if (import.meta.url === argvUrl) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
// Symlinked invocation (installed projection, /var → /private/var):
|
|
393
|
+
// compare realpaths so the entry guard still fires.
|
|
394
|
+
try {
|
|
395
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
396
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
397
|
+
return here === argv;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (_isCliEntry()) {
|
|
404
|
+
process.exitCode = main();
|
|
405
|
+
}
|