@event4u/agent-config 9.16.0 → 9.18.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 +80 -130
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/challenge-me/vision.md +5 -0
- 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/evals/rule-compliance-audit.json +46 -0
- package/dist/agent-src/commands/feature/plan.md +20 -0
- package/dist/agent-src/commands/feature/roadmap.md +19 -0
- 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/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/roadmap/create.md +29 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
- 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/plan-confidence-gate.md +133 -0
- 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/active-remediation.md +2 -0
- package/dist/agent-src/rules/architecture.md +4 -2
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-consistency.md +10 -62
- package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/cli-output-handling.md +5 -0
- package/dist/agent-src/rules/code-comment-discipline.md +3 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +4 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/laravel-routing.md +4 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +5 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +3 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +4 -0
- package/dist/agent-src/rules/source-discovery-gate.md +3 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +5 -0
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/ui-audit-gate.md +2 -0
- 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/design-intelligence/SKILL.md +6 -180
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
- package/dist/agent-src/skills/design-review/SKILL.md +4 -141
- package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
- package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
- package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
- package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
- 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/roadmap-management/SKILL.md +10 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -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 +13 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
- package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
- 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_explain.js +36 -36
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +1169 -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_route_audit.js +249 -0
- package/dist/cli-delegate/cmd_route_explain.js +285 -0
- 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 +84 -84
- 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 +34 -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/router.json +1 -1
- 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 +25 -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/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/src/config/agent-settings.template.yml +68 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +70 -0
- package/src/scripts/_cli/cmd_explain.ts +41 -36
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/cmd_route_audit.ts +290 -0
- package/src/scripts/_cli/cmd_route_explain.ts +269 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +32 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/git_env.ts +40 -0
- package/src/scripts/_lib/md_table.ts +27 -0
- package/src/scripts/_lib/planning_settings.ts +43 -0
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/_lib/router_match.ts +301 -0
- package/src/scripts/annotate_r1_outcomes.ts +258 -0
- package/src/scripts/check_completion_review.ts +1140 -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/compile_router.ts +20 -3
- package/src/scripts/dispatch_r2_reviewer.ts +968 -0
- package/src/scripts/eval_ui_triviality.ts +156 -0
- 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 +327 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +931 -0
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/lint_trigger_collisions.ts +257 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/router_telemetry.ts +16 -290
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/schemas/rule.schema.json +10 -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 +41 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session enumeration for `agent-config handoff` (road-to-agent-handoff-
|
|
3
|
+
* resume Phase 1). Pure library — no CLI entry, no prompts, no writes.
|
|
4
|
+
*
|
|
5
|
+
* Sources, merged newest-first into one list:
|
|
6
|
+
* - `chat-history` — the cross-host JSONL log (primary; works for
|
|
7
|
+
* every hook-capable host). Reuses `chat_history.list_sessions()`.
|
|
8
|
+
* - `claude-transcript` — native Claude Code transcripts under
|
|
9
|
+
* `~/.claude/projects/<slug>/*.jsonl` (fallback for sessions the log
|
|
10
|
+
* missed; slug = cwd with `/` and `.` mapped to `-`).
|
|
11
|
+
* - `codex-session` — `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`
|
|
12
|
+
* (Codex's ONLY source — a bundle host without hooks never writes
|
|
13
|
+
* chat-history). Format verified 2026-08-04 against the live store:
|
|
14
|
+
* line 0 is `{type:'session_meta', payload:{id, timestamp, cwd,
|
|
15
|
+
* git:{branch}}}`; user prompts arrive as `event_msg` records with
|
|
16
|
+
* `payload:{type:'user_message', message}` (fixture:
|
|
17
|
+
* tests/scripts/fixtures/handoff/codex-session.jsonl).
|
|
18
|
+
*
|
|
19
|
+
* De-dup: native-store sessions whose platform id maps (via
|
|
20
|
+
* `derive_session_tag`) to a chat-history bucket are dropped — the
|
|
21
|
+
* chat-history entry wins.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as os from 'node:os';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { derive_session_tag, list_sessions, read_entries } from '../chat_history.js';
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_CAP = 15;
|
|
31
|
+
const SUMMARY_SNIPPET_CHARS = 120;
|
|
32
|
+
|
|
33
|
+
export type HandoffSource = 'chat-history' | 'claude-transcript' | 'codex-session';
|
|
34
|
+
|
|
35
|
+
export interface HandoffSession {
|
|
36
|
+
id: string;
|
|
37
|
+
source: HandoffSource;
|
|
38
|
+
startedAt: string | null;
|
|
39
|
+
endedAt: string | null;
|
|
40
|
+
branch?: string;
|
|
41
|
+
entryCount: number;
|
|
42
|
+
summary: string;
|
|
43
|
+
/** Absolute transcript path — set for the native-store sources. */
|
|
44
|
+
transcriptPath?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ListOptions {
|
|
48
|
+
/** Repo root the sessions must belong to. Default: process.cwd(). */
|
|
49
|
+
cwd?: string;
|
|
50
|
+
/** Chat-history JSONL override (else AGENT_CHAT_HISTORY_FILE / default). */
|
|
51
|
+
chatHistoryPath?: string | null;
|
|
52
|
+
/** Claude Code projects root. Default: ~/.claude/projects. */
|
|
53
|
+
claudeProjectsRoot?: string;
|
|
54
|
+
/** Codex sessions root. Default: ~/.codex/sessions. */
|
|
55
|
+
codexSessionsRoot?: string;
|
|
56
|
+
/** Max sessions per source AND in the merged result. Default 15. */
|
|
57
|
+
cap?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Mirrors the Claude Code project-dir naming: `/` and `.` → `-`. */
|
|
61
|
+
export function encode_project_path(cwd: string): string {
|
|
62
|
+
return cwd.replace(/[/.]/g, '-');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _snippet(text: string, max: number = SUMMARY_SNIPPET_CHARS): string {
|
|
66
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
67
|
+
return oneLine.length <= max ? oneLine : oneLine.slice(0, max - 1) + '…';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** first-prompt → last-prompt summary (Step 3 contract). */
|
|
71
|
+
function _summarize(first: string | null, last: string | null): string {
|
|
72
|
+
const a = first ? _snippet(first) : '';
|
|
73
|
+
const b = last ? _snippet(last) : '';
|
|
74
|
+
if (a && b && a !== b) return `${a} → ${b}`;
|
|
75
|
+
return a || b || '(no user prompts)';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _read_lines(file: string): string[] {
|
|
79
|
+
try {
|
|
80
|
+
return fs.readFileSync(file, 'utf-8').split('\n');
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _parse_json(line: string): Record<string, unknown> | null {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed) return null;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
91
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
92
|
+
return parsed as Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// fail-open — skip malformed lines
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------
|
|
101
|
+
// chat-history (primary)
|
|
102
|
+
// ---------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
const CHAT_HISTORY_REL = path.join('agents', 'runtime', '.agent-chat-history');
|
|
105
|
+
|
|
106
|
+
/** Explicit option > AGENT_CHAT_HISTORY_FILE env > repo-root-relative default. */
|
|
107
|
+
function _chat_history_path(opts: ListOptions): string {
|
|
108
|
+
if (opts.chatHistoryPath) return opts.chatHistoryPath;
|
|
109
|
+
return (
|
|
110
|
+
process.env.AGENT_CHAT_HISTORY_FILE ||
|
|
111
|
+
path.join(opts.cwd ?? process.cwd(), CHAT_HISTORY_REL)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Real-prompt summaries per session: hosts log system/task notifications as
|
|
117
|
+
* `t:'user'` entries, so the raw bucket summary can read like
|
|
118
|
+
* `<task-notification>…` — filtered here (council review 2026-08-04,
|
|
119
|
+
* claude-sonnet-4-5 + gpt-4o: picker noise is a UX defect; the log itself
|
|
120
|
+
* stays unfiltered).
|
|
121
|
+
*/
|
|
122
|
+
function _chat_history_summaries(histPath: string): Map<string, { first: string | null; last: string | null }> {
|
|
123
|
+
const bySession = new Map<string, { first: string | null; last: string | null }>();
|
|
124
|
+
let entries: Array<Record<string, unknown>>;
|
|
125
|
+
try {
|
|
126
|
+
entries = read_entries({ path: histPath, last: null });
|
|
127
|
+
} catch {
|
|
128
|
+
return bySession;
|
|
129
|
+
}
|
|
130
|
+
for (const e of entries) {
|
|
131
|
+
if (e.t !== 'user' || typeof e.text !== 'string') continue;
|
|
132
|
+
const text = e.text.trim();
|
|
133
|
+
if (!text || text.startsWith('<') || text.startsWith('Caveat:')) continue;
|
|
134
|
+
const sid = String(e.s ?? '<legacy>');
|
|
135
|
+
const slot = bySession.get(sid) ?? { first: null, last: null };
|
|
136
|
+
if (!slot.first) slot.first = text;
|
|
137
|
+
slot.last = text;
|
|
138
|
+
bySession.set(sid, slot);
|
|
139
|
+
}
|
|
140
|
+
return bySession;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _chat_history_sessions(opts: ListOptions): HandoffSession[] {
|
|
144
|
+
const histPath = _chat_history_path(opts);
|
|
145
|
+
let buckets: Array<Record<string, unknown>>;
|
|
146
|
+
try {
|
|
147
|
+
buckets = list_sessions({ path: histPath, summary: true });
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const summaries = _chat_history_summaries(histPath);
|
|
152
|
+
return buckets
|
|
153
|
+
.filter((b) => Number(b.count ?? 0) > 0)
|
|
154
|
+
.map((b) => {
|
|
155
|
+
const id = String(b.id ?? '');
|
|
156
|
+
const prompts = summaries.get(id);
|
|
157
|
+
const summary = prompts
|
|
158
|
+
? _summarize(prompts.first, prompts.last)
|
|
159
|
+
: '(no user prompts)';
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
source: 'chat-history' as const,
|
|
163
|
+
startedAt: b.first_ts ? String(b.first_ts) : null,
|
|
164
|
+
endedAt: b.last_ts ? String(b.last_ts) : null,
|
|
165
|
+
entryCount: Number(b.count ?? 0),
|
|
166
|
+
summary,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------
|
|
172
|
+
// claude-transcript (native fallback)
|
|
173
|
+
// ---------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
interface TranscriptScan {
|
|
176
|
+
entryCount: number;
|
|
177
|
+
firstTs: string | null;
|
|
178
|
+
lastTs: string | null;
|
|
179
|
+
branch: string | null;
|
|
180
|
+
firstPrompt: string | null;
|
|
181
|
+
lastPrompt: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A real prompt has string content; tool_results arrive as block lists. */
|
|
185
|
+
function _cc_prompt_text(rec: Record<string, unknown>): string | null {
|
|
186
|
+
if (rec.type !== 'user' || rec.isSidechain === true) return null;
|
|
187
|
+
const msg = rec.message;
|
|
188
|
+
if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return null;
|
|
189
|
+
const content = (msg as Record<string, unknown>).content;
|
|
190
|
+
if (typeof content !== 'string') return null;
|
|
191
|
+
const trimmed = content.trim();
|
|
192
|
+
// skip host-injected meta blocks (caveats, command wrappers)
|
|
193
|
+
if (!trimmed || trimmed.startsWith('<') || trimmed.startsWith('Caveat:')) return null;
|
|
194
|
+
return trimmed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _scan_claude_transcript(file: string): TranscriptScan {
|
|
198
|
+
const scan: TranscriptScan = {
|
|
199
|
+
entryCount: 0,
|
|
200
|
+
firstTs: null,
|
|
201
|
+
lastTs: null,
|
|
202
|
+
branch: null,
|
|
203
|
+
firstPrompt: null,
|
|
204
|
+
lastPrompt: null,
|
|
205
|
+
};
|
|
206
|
+
for (const line of _read_lines(file)) {
|
|
207
|
+
const rec = _parse_json(line);
|
|
208
|
+
if (!rec) continue;
|
|
209
|
+
scan.entryCount += 1;
|
|
210
|
+
const ts = typeof rec.timestamp === 'string' ? rec.timestamp : null;
|
|
211
|
+
if (ts) {
|
|
212
|
+
if (!scan.firstTs) scan.firstTs = ts;
|
|
213
|
+
scan.lastTs = ts;
|
|
214
|
+
}
|
|
215
|
+
if (!scan.branch && typeof rec.gitBranch === 'string' && rec.gitBranch) {
|
|
216
|
+
scan.branch = rec.gitBranch;
|
|
217
|
+
}
|
|
218
|
+
const prompt = _cc_prompt_text(rec);
|
|
219
|
+
if (prompt) {
|
|
220
|
+
if (!scan.firstPrompt) scan.firstPrompt = prompt;
|
|
221
|
+
scan.lastPrompt = prompt;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return scan;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _claude_transcript_sessions(opts: ListOptions, cap: number): HandoffSession[] {
|
|
228
|
+
const root = opts.claudeProjectsRoot ?? path.join(os.homedir(), '.claude', 'projects');
|
|
229
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
230
|
+
const projectDir = path.join(root, encode_project_path(cwd));
|
|
231
|
+
let names: string[];
|
|
232
|
+
try {
|
|
233
|
+
names = fs.readdirSync(projectDir).filter((n) => n.endsWith('.jsonl'));
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const files = names
|
|
238
|
+
.map((n) => {
|
|
239
|
+
const full = path.join(projectDir, n);
|
|
240
|
+
try {
|
|
241
|
+
return { full, id: path.basename(n, '.jsonl'), mtime: fs.statSync(full).mtimeMs };
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
.filter((f): f is { full: string; id: string; mtime: number } => f !== null)
|
|
247
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
248
|
+
.slice(0, cap);
|
|
249
|
+
|
|
250
|
+
const sessions: HandoffSession[] = [];
|
|
251
|
+
for (const f of files) {
|
|
252
|
+
const scan = _scan_claude_transcript(f.full);
|
|
253
|
+
if (scan.entryCount === 0) continue;
|
|
254
|
+
sessions.push({
|
|
255
|
+
id: f.id,
|
|
256
|
+
source: 'claude-transcript',
|
|
257
|
+
startedAt: scan.firstTs,
|
|
258
|
+
endedAt: scan.lastTs,
|
|
259
|
+
...(scan.branch ? { branch: scan.branch } : {}),
|
|
260
|
+
entryCount: scan.entryCount,
|
|
261
|
+
summary: _summarize(scan.firstPrompt, scan.lastPrompt),
|
|
262
|
+
transcriptPath: f.full,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return sessions;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------
|
|
269
|
+
// codex-session (native adapter — Codex's only source)
|
|
270
|
+
// ---------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
interface CodexMeta {
|
|
273
|
+
id: string;
|
|
274
|
+
timestamp: string | null;
|
|
275
|
+
cwd: string;
|
|
276
|
+
branch: string | null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function _codex_meta(file: string): CodexMeta | null {
|
|
280
|
+
let firstLine = '';
|
|
281
|
+
try {
|
|
282
|
+
// metadata is always line 0 — never read the whole file here
|
|
283
|
+
const fd = fs.openSync(file, 'r');
|
|
284
|
+
try {
|
|
285
|
+
const buf = Buffer.alloc(8192);
|
|
286
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
287
|
+
firstLine = buf.toString('utf-8', 0, n).split('\n')[0] ?? '';
|
|
288
|
+
} finally {
|
|
289
|
+
fs.closeSync(fd);
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const rec = _parse_json(firstLine);
|
|
295
|
+
if (!rec || rec.type !== 'session_meta') return null;
|
|
296
|
+
const payload = rec.payload;
|
|
297
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
298
|
+
const p = payload as Record<string, unknown>;
|
|
299
|
+
const git =
|
|
300
|
+
p.git && typeof p.git === 'object' && !Array.isArray(p.git)
|
|
301
|
+
? (p.git as Record<string, unknown>)
|
|
302
|
+
: {};
|
|
303
|
+
return {
|
|
304
|
+
id: String(p.id ?? ''),
|
|
305
|
+
timestamp: typeof p.timestamp === 'string' ? p.timestamp : null,
|
|
306
|
+
cwd: String(p.cwd ?? ''),
|
|
307
|
+
branch: typeof git.branch === 'string' && git.branch ? git.branch : null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _codex_files(root: string): string[] {
|
|
312
|
+
const out: string[] = [];
|
|
313
|
+
const stack = [root];
|
|
314
|
+
while (stack.length > 0) {
|
|
315
|
+
const dir = stack.pop() as string;
|
|
316
|
+
let entries: fs.Dirent[];
|
|
317
|
+
try {
|
|
318
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
319
|
+
} catch {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
for (const e of entries) {
|
|
323
|
+
const full = path.join(dir, e.name);
|
|
324
|
+
if (e.isDirectory()) stack.push(full);
|
|
325
|
+
else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(full);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// rollout filenames embed the timestamp — lexicographic basename sort
|
|
329
|
+
// is chronological; newest first
|
|
330
|
+
return out.sort((a, b) => (path.basename(a) < path.basename(b) ? 1 : -1));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface CodexScan {
|
|
334
|
+
entryCount: number;
|
|
335
|
+
lastTs: string | null;
|
|
336
|
+
firstPrompt: string | null;
|
|
337
|
+
lastPrompt: string | null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function _codex_prompt_text(rec: Record<string, unknown>): string | null {
|
|
341
|
+
if (rec.type !== 'event_msg') return null;
|
|
342
|
+
const p = rec.payload;
|
|
343
|
+
if (!p || typeof p !== 'object' || Array.isArray(p)) return null;
|
|
344
|
+
const payload = p as Record<string, unknown>;
|
|
345
|
+
if (payload.type !== 'user_message') return null;
|
|
346
|
+
const msg = typeof payload.message === 'string' ? payload.message.trim() : '';
|
|
347
|
+
if (!msg || msg.startsWith('<')) return null; // skip injected meta blocks
|
|
348
|
+
return msg;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function _scan_codex_session(file: string): CodexScan {
|
|
352
|
+
const scan: CodexScan = { entryCount: 0, lastTs: null, firstPrompt: null, lastPrompt: null };
|
|
353
|
+
for (const line of _read_lines(file)) {
|
|
354
|
+
const rec = _parse_json(line);
|
|
355
|
+
if (!rec) continue;
|
|
356
|
+
scan.entryCount += 1;
|
|
357
|
+
if (typeof rec.timestamp === 'string') scan.lastTs = rec.timestamp;
|
|
358
|
+
const prompt = _codex_prompt_text(rec);
|
|
359
|
+
if (prompt) {
|
|
360
|
+
if (!scan.firstPrompt) scan.firstPrompt = prompt;
|
|
361
|
+
scan.lastPrompt = prompt;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return scan;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function _matches_repo(sessionCwd: string, repoCwd: string): boolean {
|
|
368
|
+
const norm = (p: string): string => path.resolve(p).replace(/\/+$/, '');
|
|
369
|
+
const a = norm(sessionCwd);
|
|
370
|
+
const b = norm(repoCwd);
|
|
371
|
+
return a === b || a.startsWith(b + path.sep);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function _codex_sessions(opts: ListOptions, cap: number): HandoffSession[] {
|
|
375
|
+
const root = opts.codexSessionsRoot ?? path.join(os.homedir(), '.codex', 'sessions');
|
|
376
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
377
|
+
const matching: Array<{ file: string; meta: CodexMeta }> = [];
|
|
378
|
+
for (const file of _codex_files(root)) {
|
|
379
|
+
if (matching.length >= cap) break;
|
|
380
|
+
const meta = _codex_meta(file); // line-0 read only — cheap per file
|
|
381
|
+
if (!meta || !meta.id) continue;
|
|
382
|
+
if (!_matches_repo(meta.cwd, cwd)) continue;
|
|
383
|
+
matching.push({ file, meta });
|
|
384
|
+
}
|
|
385
|
+
const sessions: HandoffSession[] = [];
|
|
386
|
+
for (const { file, meta } of matching) {
|
|
387
|
+
const scan = _scan_codex_session(file);
|
|
388
|
+
if (scan.entryCount === 0) continue;
|
|
389
|
+
sessions.push({
|
|
390
|
+
id: meta.id,
|
|
391
|
+
source: 'codex-session',
|
|
392
|
+
startedAt: meta.timestamp,
|
|
393
|
+
endedAt: scan.lastTs ?? meta.timestamp,
|
|
394
|
+
...(meta.branch ? { branch: meta.branch } : {}),
|
|
395
|
+
entryCount: scan.entryCount,
|
|
396
|
+
summary: _summarize(scan.firstPrompt, scan.lastPrompt),
|
|
397
|
+
transcriptPath: file,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return sessions;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------
|
|
404
|
+
// merge
|
|
405
|
+
// ---------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
function _sort_key(s: HandoffSession): string {
|
|
408
|
+
return s.endedAt ?? s.startedAt ?? '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Unified newest-first session list. Native-store sessions whose platform
|
|
413
|
+
* id maps to an existing chat-history bucket are dropped (chat-history is
|
|
414
|
+
* the primary source).
|
|
415
|
+
*/
|
|
416
|
+
export function list_handoff_sessions(opts: ListOptions = {}): HandoffSession[] {
|
|
417
|
+
const cap = opts.cap ?? DEFAULT_CAP;
|
|
418
|
+
const primary = _chat_history_sessions(opts);
|
|
419
|
+
const knownTags = new Set(primary.map((s) => s.id));
|
|
420
|
+
|
|
421
|
+
const native = [
|
|
422
|
+
..._claude_transcript_sessions(opts, cap),
|
|
423
|
+
..._codex_sessions(opts, cap),
|
|
424
|
+
].filter((s) => {
|
|
425
|
+
try {
|
|
426
|
+
return !knownTags.has(derive_session_tag(s.id));
|
|
427
|
+
} catch {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return [...primary, ...native]
|
|
433
|
+
.sort((a, b) => (_sort_key(a) < _sort_key(b) ? 1 : _sort_key(a) > _sort_key(b) ? -1 : 0))
|
|
434
|
+
.slice(0, cap);
|
|
435
|
+
}
|
|
@@ -208,6 +208,15 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
208
208
|
router + projection freshness, host bridge status.
|
|
209
209
|
Flags: --platform <p>, --workspace <path>, --json, --strict,
|
|
210
210
|
--no-freshness
|
|
211
|
+
route:explain Deterministic rule-routing trace for one prompt: matched
|
|
212
|
+
triggers, tier, injected-vs-pointer disposition, budget
|
|
213
|
+
consumption, rejected candidates. Trigger-match level only
|
|
214
|
+
(what the host invokes is not measured).
|
|
215
|
+
Flags: --files a,b, --profile full|balanced, --json
|
|
216
|
+
route:audit Replay the router matcher over the last N user prompts of
|
|
217
|
+
the chat-history log; renders matched rules per prompt.
|
|
218
|
+
Trigger-match level only; read-only, no LLM call.
|
|
219
|
+
Flags: --last N, --record (opt-in recorder), --weekly, --json
|
|
211
220
|
hooks:replay Replay a fixture through the universal dispatcher with
|
|
212
221
|
AGENT_CONFIG_REPLAY=1 (no writes under agents/runtime/state/).
|
|
213
222
|
Usage: hooks:replay --platform <name> --event <event>
|
|
@@ -257,6 +266,7 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
257
266
|
telemetry:status Print artefact-engagement telemetry status (read-only)
|
|
258
267
|
telemetry:report Aggregate the engagement log into a quartile report
|
|
259
268
|
analyze-session Read-only post-session report from on-disk runtime state
|
|
269
|
+
handoff Pick a recent session, generate a handoff, seed a fresh session
|
|
260
270
|
EOF
|
|
261
271
|
fi
|
|
262
272
|
|
|
@@ -1170,6 +1180,18 @@ cmd_versions() {
|
|
|
1170
1180
|
|
|
1171
1181
|
# `agent-config explain <config|rule|route>` — print the decision chain
|
|
1172
1182
|
# behind a configuration or routing outcome. Read-only diagnostic; never
|
|
1183
|
+
# Router-matcher replay over recent chat-history prompts (read-only).
|
|
1184
|
+
# See scripts/_cli/cmd_route_audit.ts.
|
|
1185
|
+
cmd_route_audit() {
|
|
1186
|
+
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_route_audit.ts" "$@"
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
# Deterministic rule-routing trace (trigger-match measurement level only).
|
|
1190
|
+
# See scripts/_cli/cmd_route_explain.ts.
|
|
1191
|
+
cmd_route_explain() {
|
|
1192
|
+
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_route_explain.ts" "$@"
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1173
1195
|
# edits state. See scripts/_cli/cmd_explain.ts.
|
|
1174
1196
|
cmd_explain() {
|
|
1175
1197
|
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_explain.ts" "$@"
|
|
@@ -1182,6 +1204,13 @@ cmd_analyze_session() {
|
|
|
1182
1204
|
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_analyze_session.ts" "$@"
|
|
1183
1205
|
}
|
|
1184
1206
|
|
|
1207
|
+
# `agent-config handoff` — pick a recent session, generate a deterministic
|
|
1208
|
+
# handoff into agents/runtime/state/handoff-context.md, optionally --launch a
|
|
1209
|
+
# fresh host session. See scripts/_cli/cmd_handoff.ts.
|
|
1210
|
+
cmd_handoff() {
|
|
1211
|
+
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_handoff.ts" "$@"
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1185
1214
|
main() {
|
|
1186
1215
|
local cmd="${1-}"
|
|
1187
1216
|
[[ $# -gt 0 ]] && shift || true
|
|
@@ -1227,6 +1256,8 @@ main() {
|
|
|
1227
1256
|
hooks:status) cmd_hooks_status "$@" ;;
|
|
1228
1257
|
hooks:doctor) cmd_hooks_doctor "$@" ;;
|
|
1229
1258
|
routing:doctor) cmd_routing_doctor "$@" ;;
|
|
1259
|
+
route:explain) cmd_route_explain "$@" ;;
|
|
1260
|
+
route:audit) cmd_route_audit "$@" ;;
|
|
1230
1261
|
hooks:replay) cmd_hooks_replay "$@" ;;
|
|
1231
1262
|
reach:doctor) cmd_reach_doctor "$@" ;;
|
|
1232
1263
|
telemetry:record) cmd_telemetry_record "$@" ;;
|
|
@@ -1255,6 +1286,7 @@ main() {
|
|
|
1255
1286
|
versions) cmd_versions "$@" ;;
|
|
1256
1287
|
explain) cmd_explain "$@" ;;
|
|
1257
1288
|
analyze-session) cmd_analyze_session "$@" ;;
|
|
1289
|
+
handoff) cmd_handoff "$@" ;;
|
|
1258
1290
|
help|--help|-h|"")
|
|
1259
1291
|
# Optional `--tier=0|1|all` filter (default 0).
|
|
1260
1292
|
local tier_arg="0"
|
|
@@ -179,6 +179,27 @@ function _iterdirSorted(p: string): string[] {
|
|
|
179
179
|
function _rglobSorted(root: string, pattern: string): string[] {
|
|
180
180
|
const out: string[] = [];
|
|
181
181
|
const suffix = pattern === '*' ? null : pattern.slice(1); // drop leading '*'
|
|
182
|
+
// Symlink confinement: every symlink (leaf or directory) must resolve
|
|
183
|
+
// inside the walk root, or it is ignored. A visited-realpath set bounds
|
|
184
|
+
// symlink cycles, and a broken symlink (realpath throws) is skipped
|
|
185
|
+
// explicitly. Without this, a directory symlink planted under a source
|
|
186
|
+
// root walks arbitrary paths outside the package and a cycle recurses
|
|
187
|
+
// until stack exhaustion (release-truth Phase 3).
|
|
188
|
+
let rootReal: string;
|
|
189
|
+
try {
|
|
190
|
+
rootReal = fs.realpathSync(root);
|
|
191
|
+
} catch {
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
const _confined = (p: string): string | null => {
|
|
195
|
+
try {
|
|
196
|
+
const real = fs.realpathSync(p);
|
|
197
|
+
return real === rootReal || _isUnder(real, rootReal) ? real : null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null; // broken symlink → handled explicitly as "not yielded"
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const visited = new Set<string>([rootReal]);
|
|
182
203
|
const walk = (dir: string): void => {
|
|
183
204
|
let entries: fs.Dirent[];
|
|
184
205
|
try {
|
|
@@ -189,14 +210,21 @@ function _rglobSorted(root: string, pattern: string): string[] {
|
|
|
189
210
|
for (const ent of entries) {
|
|
190
211
|
const full = path.join(dir, ent.name);
|
|
191
212
|
const matches = suffix === null ? true : ent.name.endsWith(suffix);
|
|
192
|
-
if (matches) {
|
|
213
|
+
if (matches && (!ent.isSymbolicLink() || _confined(full) !== null)) {
|
|
193
214
|
out.push(full);
|
|
194
215
|
}
|
|
195
216
|
// Recurse into directories. `isDirectory()` on a Dirent does not
|
|
196
|
-
// follow symlinks; pathlib.rglob follows directory symlinks.
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
if (ent.isDirectory()
|
|
217
|
+
// follow symlinks; pathlib.rglob follows directory symlinks. Follow
|
|
218
|
+
// a directory symlink only when its target stays inside the walk
|
|
219
|
+
// root and has not been visited yet (cycle guard).
|
|
220
|
+
if (ent.isDirectory()) {
|
|
221
|
+
walk(full);
|
|
222
|
+
} else if (ent.isSymbolicLink() && _isDir(full)) {
|
|
223
|
+
const real = _confined(full);
|
|
224
|
+
if (real === null || visited.has(real)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
visited.add(real);
|
|
200
228
|
walk(full);
|
|
201
229
|
}
|
|
202
230
|
}
|
|
@@ -432,7 +460,9 @@ export function artefact_roots(): string[] {
|
|
|
432
460
|
*
|
|
433
461
|
* Walks `_root_specs` (the leaf view). Deterministic order; deduplicated on
|
|
434
462
|
* logical path so a file present in two roots during the move window is
|
|
435
|
-
* yielded once.
|
|
463
|
+
* yielded once. Non-files are skipped; symlinks are followed only when they
|
|
464
|
+
* resolve inside their walk root (external targets, broken links, and cycles
|
|
465
|
+
* are ignored). Mirrors `iter_artefacts`.
|
|
436
466
|
*/
|
|
437
467
|
export function* iter_artefacts(suffix = '.md'): Generator<string> {
|
|
438
468
|
const seen: Set<string> = new Set();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for child `git` invocations that must resolve from their `cwd`.
|
|
3
|
+
*
|
|
4
|
+
* Git hooks export `GIT_DIR` (and often `GIT_INDEX_FILE`), and every child
|
|
5
|
+
* process inherits them. An inherited `GIT_DIR` **overrides** repository
|
|
6
|
+
* discovery, so `execFileSync('git', […], { cwd })` silently operates against
|
|
7
|
+
* the hook's repository instead of `cwd`. In a linked worktree the two differ,
|
|
8
|
+
* and the symptom is not an error but wrong data: `git log -- <path>` returns
|
|
9
|
+
* nothing, which the Gate R1 grandfather clause reads as "no pre-activation
|
|
10
|
+
* baseline" and reports as `missing_register` on every grandfathered roadmap.
|
|
11
|
+
*
|
|
12
|
+
* That made the pre-push enforcement layer fire spuriously while CI — which
|
|
13
|
+
* runs the same gate WITHOUT `GIT_DIR` — stayed green: the worst shape for a
|
|
14
|
+
* gate, since the failure looks like a content problem and the authoritative
|
|
15
|
+
* layer disagrees with the local one.
|
|
16
|
+
*
|
|
17
|
+
* Every gate in this family therefore runs git through this env: the variables
|
|
18
|
+
* that redirect discovery are removed, so `cwd` is the only thing that decides.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Git env vars that override repository discovery and must not be inherited. */
|
|
22
|
+
export const GIT_DISCOVERY_VARS: readonly string[] = [
|
|
23
|
+
'GIT_DIR',
|
|
24
|
+
'GIT_WORK_TREE',
|
|
25
|
+
'GIT_INDEX_FILE',
|
|
26
|
+
'GIT_OBJECT_DIRECTORY',
|
|
27
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
28
|
+
'GIT_COMMON_DIR',
|
|
29
|
+
'GIT_NAMESPACE',
|
|
30
|
+
'GIT_PREFIX',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** `process.env` minus the discovery-overriding variables. */
|
|
34
|
+
export function gitEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
35
|
+
const env: NodeJS.ProcessEnv = { ...base };
|
|
36
|
+
for (const key of GIT_DISCOVERY_VARS) {
|
|
37
|
+
delete env[key];
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown table-row splitting, escape-aware.
|
|
3
|
+
*
|
|
4
|
+
* Both plan-governance validators parse pipe tables whose cells legitimately
|
|
5
|
+
* contain an escaped pipe: the Risk-Register contract example writes
|
|
6
|
+
* `product \| implementation`, and findings text routinely describes unions or
|
|
7
|
+
* flags (`text\|json`). Splitting on every `|` shifts every later cell by one,
|
|
8
|
+
* so `Risk type` becomes `product \` and a findings row's `Status` becomes a
|
|
9
|
+
* fragment of its own Finding text — a spurious block with no fix except
|
|
10
|
+
* rewording the prose.
|
|
11
|
+
*
|
|
12
|
+
* One definition, imported by both validators: the same defect appearing twice
|
|
13
|
+
* is what made this a shared helper rather than a local function.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Split on unescaped `|` only, drop the outer delimiters, unescape, trim. */
|
|
17
|
+
export function splitMarkdownRow(line: string): string[] {
|
|
18
|
+
let inner = line.trim();
|
|
19
|
+
if (inner.startsWith('|')) {
|
|
20
|
+
inner = inner.slice(1);
|
|
21
|
+
}
|
|
22
|
+
// A trailing pipe is a delimiter only when it is not itself escaped.
|
|
23
|
+
if (inner.endsWith('|') && !inner.endsWith('\\|')) {
|
|
24
|
+
inner = inner.slice(0, -1);
|
|
25
|
+
}
|
|
26
|
+
return inner.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, '|').trim());
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate-R2 settings escape hatch, shared by the validator and the dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* It lives here rather than in either script because BOTH R2 enforcement layers
|
|
5
|
+
* must honour it: `check_completion_review` (the gate) and
|
|
6
|
+
* `dispatch_r2_reviewer --verify` / `--verify-current` (the manifest
|
|
7
|
+
* re-derivation CI step). A reader that only the gate consulted left the second,
|
|
8
|
+
* blocking layer firing with the gate nominally switched off. `check_*` already
|
|
9
|
+
* imports the scope hash from `dispatch_*`, so putting the predicate in either
|
|
10
|
+
* one would make the import cycle back on itself.
|
|
11
|
+
*
|
|
12
|
+
* Fail-open by design: an unreadable or unparseable settings file, or a missing
|
|
13
|
+
* key, leaves the gate ACTIVE (`=== false` is the only disabling value).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
|
|
21
|
+
function planningFlagIsFalse(settingsDir: string, key: string): boolean {
|
|
22
|
+
const settingsPath = path.join(settingsDir, '.agent-settings.yml');
|
|
23
|
+
let raw: string;
|
|
24
|
+
try {
|
|
25
|
+
raw = fs.readFileSync(settingsPath, 'utf-8');
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const parsed = parseYaml(raw) as unknown;
|
|
31
|
+
if (parsed === null || typeof parsed !== 'object') return false;
|
|
32
|
+
const planning = (parsed as Record<string, unknown>)['planning'];
|
|
33
|
+
if (planning === null || typeof planning !== 'object') return false;
|
|
34
|
+
return (planning as Record<string, unknown>)[key] === false;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `planning.completion_review === false` in `<dir>/.agent-settings.yml`. */
|
|
41
|
+
export function completionReviewDisabled(settingsDir: string): boolean {
|
|
42
|
+
return planningFlagIsFalse(settingsDir, 'completion_review');
|
|
43
|
+
}
|