aiwcli 0.12.1 → 0.12.2

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.
Files changed (57) hide show
  1. package/dist/templates/_shared/hooks-ts/session_start.ts +21 -15
  2. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +20 -8
  3. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +151 -29
  4. package/dist/templates/_shared/scripts/resume_handoff.ts +25 -0
  5. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -7
  6. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-EVOLUTION.md +62 -63
  7. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-PATTERNS.md +61 -62
  8. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-STRUCTURE.md +62 -63
  9. package/dist/templates/cc-native/_cc-native/agents/plan-review/ASSUMPTION-TRACER.md +56 -57
  10. package/dist/templates/cc-native/_cc-native/agents/plan-review/CLARITY-AUDITOR.md +53 -54
  11. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -67
  12. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-GAPS.md +70 -71
  13. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-ORDERING.md +62 -63
  14. package/dist/templates/cc-native/_cc-native/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -73
  15. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -62
  16. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -65
  17. package/dist/templates/cc-native/_cc-native/agents/plan-review/DEVILS-ADVOCATE.md +56 -57
  18. package/dist/templates/cc-native/_cc-native/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -87
  19. package/dist/templates/cc-native/_cc-native/agents/plan-review/HANDOFF-READINESS.md +59 -60
  20. package/dist/templates/cc-native/_cc-native/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -59
  21. package/dist/templates/cc-native/_cc-native/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -67
  22. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-DEPENDENCY.md +62 -63
  23. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-FMEA.md +66 -67
  24. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-PREMORTEM.md +71 -72
  25. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-REVERSIBILITY.md +74 -75
  26. package/dist/templates/cc-native/_cc-native/agents/plan-review/SCOPE-BOUNDARY.md +77 -78
  27. package/dist/templates/cc-native/_cc-native/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -63
  28. package/dist/templates/cc-native/_cc-native/agents/plan-review/SKEPTIC.md +68 -69
  29. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -62
  30. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -72
  31. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -62
  32. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -62
  33. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-COSTS.md +67 -68
  34. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -66
  35. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-COVERAGE.md +74 -75
  36. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-STRENGTH.md +69 -70
  37. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +19 -2
  38. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +28 -1010
  39. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -0
  40. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +1 -2
  41. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -0
  42. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -0
  43. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -0
  44. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -0
  45. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +19 -821
  46. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +36 -13
  47. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -0
  48. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +1 -2
  49. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +489 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -1
  53. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +51 -17
  55. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +40 -2
  56. package/oclif.manifest.json +1 -1
  57. package/package.json +1 -1
@@ -4,6 +4,9 @@
4
4
  * See cc-native-plan-review-spec.md §4.5
5
5
  */
6
6
 
7
+ import { getContextBySessionId, saveState } from "../../_shared/lib-ts/context/context-store.js";
8
+ import { logInfo, logWarn } from "../../_shared/lib-ts/base/logger.js";
9
+ import { nowIso } from "../../_shared/lib-ts/base/utils.js";
7
10
  import type {
8
11
  CcNativeState,
9
12
  PlanReviewState,
@@ -11,9 +14,6 @@ import type {
11
14
  IterationState,
12
15
  StuckDetectionState,
13
16
  } from "./types.js";
14
- import { logInfo, logWarn } from "../../_shared/lib-ts/base/logger.js";
15
- import { nowIso } from "../../_shared/lib-ts/base/utils.js";
16
- import { getContextBySessionId, saveState } from "../../_shared/lib-ts/context/context-store.js";
17
17
  import type { ContextState } from "../../_shared/lib-ts/types.js";
18
18
 
19
19
  /**
@@ -62,8 +62,8 @@ export function saveCcNativeState(
62
62
  saveState(state.id, state, projectRoot);
63
63
  return true;
64
64
  }
65
- } catch (error: unknown) {
66
- logWarn("utils", `Failed to save cc_native state: ${error}`);
65
+ } catch (e: unknown) {
66
+ logWarn("utils", `Failed to save cc_native state: ${e}`);
67
67
  }
68
68
  return false;
69
69
  }
@@ -145,9 +145,9 @@ export function markPlanReviewed(
145
145
  max: iterationState.max ?? 1,
146
146
  complexity: iterationState.complexity ?? "unknown",
147
147
  };
148
- const {history} = iterationState;
148
+ const history = iterationState.history;
149
149
  if (history && history.length > 0) {
150
- const lastEntry = history.at(-1);
150
+ const lastEntry = history[history.length - 1];
151
151
  if (lastEntry) {
152
152
  reviewData.iteration.latest_verdict = lastEntry.verdict ?? "unknown";
153
153
  }
@@ -167,8 +167,8 @@ export function markPlanReviewed(
167
167
  `Failed to save plan review state for session ${sessionId}`,
168
168
  );
169
169
  }
170
- } catch (error: unknown) {
171
- logWarn(hookName, `Failed to mark plan reviewed: ${error}`);
170
+ } catch (e: unknown) {
171
+ logWarn(hookName, `Failed to mark plan reviewed: ${e}`);
172
172
  }
173
173
  }
174
174
 
@@ -255,8 +255,31 @@ export function markQuestionsAsked(
255
255
  ccNative.questions_asked.asked_at = timestamp;
256
256
 
257
257
  return saveCcNativeState(sessionId, projectRoot, ccNative);
258
- } catch (error: unknown) {
259
- logWarn("utils", `Failed to mark questions asked: ${error}`);
258
+ } catch (e: unknown) {
259
+ logWarn("utils", `Failed to mark questions asked: ${e}`);
260
+ return false;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Reset plan questions agent flag so a new plan gets fresh questions.
266
+ * Called when plan file path changes (new plan topic detected).
267
+ */
268
+ export function resetPlanQuestionsAsked(
269
+ sessionId: string,
270
+ projectRoot: string,
271
+ ): boolean {
272
+ try {
273
+ const ccNative = getCcNativeState(sessionId, projectRoot) ?? {};
274
+ if (ccNative.questions_asked) {
275
+ ccNative.questions_asked.plan_questions_agent_asked = {
276
+ asked: false,
277
+ asked_at: "",
278
+ };
279
+ }
280
+ return saveCcNativeState(sessionId, projectRoot, ccNative);
281
+ } catch (e: unknown) {
282
+ logWarn("utils", `Failed to reset plan questions asked: ${e}`);
260
283
  return false;
261
284
  }
262
285
  }
@@ -288,8 +311,8 @@ export function updateStuckDetectionState(
288
311
  const ccNative = getCcNativeState(sessionId, projectRoot) ?? {};
289
312
  ccNative.stuck_detection = stuckState;
290
313
  return saveCcNativeState(sessionId, projectRoot, ccNative);
291
- } catch (error: unknown) {
292
- logWarn("utils", `Failed to update stuck detection state: ${error}`);
314
+ } catch (e: unknown) {
315
+ logWarn("utils", `Failed to update stuck detection state: ${e}`);
293
316
  return false;
294
317
  }
295
318
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Graduation logic: pass eligibility, pass streaks, graduation threshold, iteration advancement.
3
+ * Extracted from cc-native-plan-review.ts.
4
+ */
5
+
6
+ import type {
7
+ IterationState,
8
+ ReviewerResult,
9
+ CombinedReviewResult,
10
+ IterationAdvancement,
11
+ } from "./types.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Pass Eligibility
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Determine which agents are pass-eligible this iteration.
19
+ * Criteria: verdict === "pass" OR zero high-severity issues.
20
+ * Agents with "skip"/"error" are NOT eligible (no signal).
21
+ */
22
+ export function computePassEligible(agentResults: Record<string, ReviewerResult>): string[] {
23
+ const eligible: string[] = [];
24
+ for (const [name, result] of Object.entries(agentResults)) {
25
+ if (result.verdict === "skip" || result.verdict === "error") continue;
26
+ if (result.verdict === "pass") { eligible.push(name); continue; }
27
+ const issues = Array.isArray(result.data?.issues)
28
+ ? (result.data.issues as Array<{ severity?: string }>) : [];
29
+ if (issues.filter(i => i.severity === "high").length === 0) {
30
+ eligible.push(name);
31
+ }
32
+ }
33
+ return eligible;
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Tracker Issue Extraction
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Extract top high-severity issues for the review tracker.
42
+ */
43
+ export function extractTopIssuesForTracker(
44
+ combined: CombinedReviewResult,
45
+ maxCount = 5,
46
+ ): string[] {
47
+ const allReviewers = Object.values(combined.agents);
48
+ const issues: string[] = [];
49
+ for (const r of allReviewers) {
50
+ if (!r.data) continue;
51
+ const issueList = r.data.issues as Array<Record<string, unknown>> | undefined;
52
+ if (!issueList) continue;
53
+ for (const issue of issueList) {
54
+ if (issue.severity === "high") {
55
+ const text = String(issue.issue ?? "").trim();
56
+ if (text) {
57
+ issues.push(`[${r.name}] ${text}`);
58
+ }
59
+ }
60
+ }
61
+ if (issues.length >= maxCount) break;
62
+ }
63
+ return issues.slice(0, maxCount);
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Iteration Advancement
68
+ // ---------------------------------------------------------------------------
69
+
70
+ const GRADUATION_THRESHOLD = 2;
71
+
72
+ /**
73
+ * Advance iteration state after a review cycle. Returns a new state copy
74
+ * (does not mutate input).
75
+ *
76
+ * - On pass/warrant: sets current past max (stop iterating)
77
+ * - On deny: increments current toward max (safety valve)
78
+ * - Updates pass streaks and graduates agents that reached threshold
79
+ */
80
+ export function advanceIterationState(
81
+ state: IterationState,
82
+ planHash: string,
83
+ planPath: string,
84
+ verdict: string,
85
+ shouldDeny: boolean,
86
+ passEligible: string[],
87
+ agentResults: Record<string, ReviewerResult>,
88
+ graduationThreshold = GRADUATION_THRESHOLD,
89
+ ): IterationAdvancement {
90
+ const updated: IterationState = {
91
+ ...state,
92
+ history: [...state.history, { hash: planHash, verdict, timestamp: new Date().toISOString() }],
93
+ lastPlanHash: planHash,
94
+ lastPlanPath: planPath,
95
+ graduated: [...state.graduated],
96
+ passStreaks: { ...(state.passStreaks ?? {}) },
97
+ };
98
+
99
+ if (!shouldDeny) {
100
+ // Pass/warrant: stop iterating
101
+ updated.current = updated.max + 1;
102
+ } else {
103
+ // Deny: advance toward max
104
+ updated.current = state.current + 1;
105
+ }
106
+
107
+ // Update pass streaks — only for agents that actually ran this iteration
108
+ const passEligibleSet = new Set(passEligible);
109
+ const graduatedSet = new Set(updated.graduated);
110
+
111
+ for (const name of Object.keys(agentResults)) {
112
+ if (graduatedSet.has(name)) continue;
113
+ if (passEligibleSet.has(name)) {
114
+ updated.passStreaks[name] = (updated.passStreaks[name] ?? 0) + 1;
115
+ } else {
116
+ updated.passStreaks[name] = 0;
117
+ }
118
+ }
119
+
120
+ // Graduate agents that reached threshold
121
+ const newGraduates: string[] = [];
122
+ for (const [name, streak] of Object.entries(updated.passStreaks)) {
123
+ if (streak >= graduationThreshold && !graduatedSet.has(name)) {
124
+ newGraduates.push(name);
125
+ }
126
+ }
127
+ if (newGraduates.length > 0) {
128
+ updated.graduated = [...updated.graduated, ...newGraduates];
129
+ }
130
+
131
+ return { updatedState: updated, newGraduates };
132
+ }
@@ -36,7 +36,6 @@ export async function runOrchestrator(
36
36
  model: config.model,
37
37
  provider: "claude",
38
38
  focus: "plan analysis and agent selection",
39
- enabled: config.enabled,
40
39
  categories: [],
41
40
  description: "Plan orchestrator",
42
41
  system_prompt: "",
@@ -58,7 +57,7 @@ export async function runOrchestrator(
58
57
  return result;
59
58
  } catch (error) {
60
59
  logWarn("orchestrator", `Unexpected error: ${error}`);
61
- const nonMandatory = agentLibrary.filter((a) => a.enabled && !mandatory.has(a.name));
60
+ const nonMandatory = agentLibrary.filter((a) => !mandatory.has(a.name));
62
61
  const fallbackCount = ((settings.agentSelection as Record<string, unknown>)?.fallbackCount as number) ?? 2;
63
62
  return {
64
63
  complexity: "medium",
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Output construction: issue truncation, verdict override, message building.
3
+ * Extracted from cc-native-plan-review.ts.
4
+ */
5
+
6
+ import { logInfo } from "../../_shared/lib-ts/base/logger.js";
7
+
8
+ import type {
9
+ ReviewerResult,
10
+ CombinedReviewResult,
11
+ CorroborationResult,
12
+ IterationState,
13
+ } from "./types.js";
14
+ import {
15
+ buildInlineReviewSummary,
16
+ extractTopIssuesText,
17
+ } from "./artifacts/format.js";
18
+ import { extractTopIssuesForTracker } from "./graduation.js";
19
+
20
+ const HOOK = "output-builder";
21
+
22
+ const REVIEWER_CAVEAT = "Reviewers have limited context compared to your full session — use your judgment to adopt valid points and dismiss genuine false positives. However, treat false positives as a clarity signal: if a reviewer misunderstood your plan, an agent executing it will likely hit the same confusion. Revise those sections to be unambiguous so no future reader — human or AI — makes the same mistake.";
23
+ const RESUBMIT_INSTRUCTION = "IMPORTANT: After revising the plan file, you MUST call ExitPlanMode again to trigger re-review. Do not end your turn or ask the user without calling ExitPlanMode.";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Issue Truncation
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Truncate per-agent issues to top N by severity.
31
+ * @mutates agentResults[name].data.issues in place.
32
+ */
33
+ export function truncateAgentIssues(
34
+ agentResults: Record<string, ReviewerResult>,
35
+ maxPerAgent: number,
36
+ ): void {
37
+ for (const r of Object.values(agentResults)) {
38
+ if (!Array.isArray(r.data?.issues)) continue;
39
+ const issues = r.data.issues as Array<{ severity?: string }>;
40
+ if (issues.length <= maxPerAgent) continue;
41
+ const severityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
42
+ issues.sort((a, b) => (severityOrder[a.severity ?? "low"] ?? 2) - (severityOrder[b.severity ?? "low"] ?? 2));
43
+ const originalCount = issues.length;
44
+ r.data.issues = issues.slice(0, maxPerAgent);
45
+ logInfo(HOOK, `${r.name}: truncated issues ${originalCount} → ${maxPerAgent}`);
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Verdict Override
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Override verdict to "fail" for agents exceeding high-issue threshold.
55
+ * @mutates agentResults[name].verdict in place.
56
+ */
57
+ export function overrideVerdictsByThreshold(
58
+ agentResults: Record<string, ReviewerResult>,
59
+ threshold: number,
60
+ ): void {
61
+ for (const r of Object.values(agentResults)) {
62
+ if (!r.verdict || r.verdict === "skip" || r.verdict === "error") continue;
63
+ const issues = Array.isArray(r.data?.issues) ? r.data.issues as Array<{ severity?: string }> : [];
64
+ const agentHigh = issues.filter(i => i.severity === "high").length;
65
+ if (agentHigh >= threshold) {
66
+ logInfo(HOOK, `${r.name}: verdict overridden to 'fail' (${agentHigh} high issues >= ${threshold})`);
67
+ r.verdict = "fail";
68
+ }
69
+ }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Output Building
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export interface ReviewOutputParams {
77
+ combinedResult: CombinedReviewResult;
78
+ corroborationResult: CorroborationResult;
79
+ iterationState: IterationState;
80
+ reviewFile: string;
81
+ highIssuesPath: string;
82
+ }
83
+
84
+ export interface ReviewOutput {
85
+ contextText: string;
86
+ blockReason: string;
87
+ shouldDeny: boolean;
88
+ }
89
+
90
+ /**
91
+ * Build the final review output: context text and block reason.
92
+ */
93
+ export function buildReviewOutput(params: ReviewOutputParams): ReviewOutput {
94
+ const { combinedResult, corroborationResult, iterationState } = params;
95
+
96
+ const shouldDeny = corroborationResult.blocking.length > 0;
97
+ const reviewScore = shouldDeny ? 1.0 : 0.0;
98
+ const overall = corroborationResult.verdict;
99
+
100
+ // Build inline summary
101
+ const inlineSummary = buildInlineReviewSummary(combinedResult, 5, 800, corroborationResult);
102
+ const contextParts = [inlineSummary];
103
+
104
+ // Top issues
105
+ const topIssuesList = extractTopIssuesForTracker(combinedResult, 5);
106
+ if (topIssuesList.length > 0) {
107
+ contextParts.push(`\nTop high-severity issues:\n${topIssuesList.map(i => `- ${i}`).join("\n")}`);
108
+ }
109
+ contextParts.push(`\nFull review: \`${params.reviewFile}\`\n`);
110
+
111
+ const contextText = contextParts.join("");
112
+ const iterInfo = ` (iteration ${iterationState.current}/${iterationState.max}, score=${reviewScore.toFixed(2)})`;
113
+
114
+ let blockReason: string;
115
+ if (shouldDeny) {
116
+ const topIssuesText = extractTopIssuesText(combinedResult, 3, "high");
117
+ blockReason = `Plan review FAILED${iterInfo}. ` +
118
+ `Critical issues: ${topIssuesText}. ` +
119
+ `IMPORTANT: Read \`${params.highIssuesPath}\` for ALL high-severity issues — ` +
120
+ `this file contains only the most critical findings, no noise. ` +
121
+ `${REVIEWER_CAVEAT} ` +
122
+ `Revise the plan to address these issues, then call ExitPlanMode again. ` +
123
+ RESUBMIT_INSTRUCTION;
124
+ } else {
125
+ blockReason = `Plan review ${overall.toUpperCase()}${iterInfo}. Review complete. ${REVIEWER_CAVEAT}`;
126
+ }
127
+
128
+ return { contextText, blockReason, shouldDeny };
129
+ }
130
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Plan file discovery: find, read, and hash the plan file.
3
+ * Extracted from cc-native-plan-review.ts.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import * as os from "node:os";
9
+ import * as crypto from "node:crypto";
10
+
11
+ import { logInfo, logDebug } from "../../_shared/lib-ts/base/logger.js";
12
+ import { findPlanPathInTranscript } from "../../_shared/lib-ts/context/plan-manager.js";
13
+
14
+ import type { DiscoveredPlan } from "./types.js";
15
+
16
+ const HOOK = "plan-discovery";
17
+
18
+ /**
19
+ * Find the most recently modified plan file in ~/.claude/plans/.
20
+ */
21
+ export function findPlanFile(): string | null {
22
+ const plansDir = path.join(os.homedir(), ".claude", "plans");
23
+ if (!fs.existsSync(plansDir)) return null;
24
+ const files = fs.readdirSync(plansDir)
25
+ .filter(f => f.endsWith(".md"))
26
+ .map(f => {
27
+ const p = path.join(plansDir, f);
28
+ return { path: p, mtime: fs.statSync(p).mtimeMs };
29
+ })
30
+ .sort((a, b) => b.mtime - a.mtime);
31
+ return files.length > 0 ? files[0]!.path : null;
32
+ }
33
+
34
+ /**
35
+ * Compute a short SHA-256 hash of plan content.
36
+ */
37
+ export function computePlanHash(content: string): string {
38
+ return crypto.createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
39
+ }
40
+
41
+ /**
42
+ * Discover the plan file, read its content, and compute its hash.
43
+ * Prefers transcript-based discovery (session-accurate), falls back to mtime scan.
44
+ */
45
+ export function discoverPlan(transcriptPath?: string): DiscoveredPlan | null {
46
+ let planPath: string | null = null;
47
+
48
+ if (transcriptPath) {
49
+ planPath = findPlanPathInTranscript(transcriptPath);
50
+ if (planPath) {
51
+ logInfo(HOOK, `Found plan via transcript: ${planPath}`);
52
+ } else {
53
+ logDebug(HOOK, "No plan Write found in transcript, falling back to mtime scan");
54
+ }
55
+ }
56
+
57
+ if (!planPath) {
58
+ planPath = findPlanFile();
59
+ }
60
+
61
+ if (!planPath) return null;
62
+
63
+ let content: string;
64
+ try {
65
+ content = fs.readFileSync(planPath, "utf-8").trim();
66
+ } catch {
67
+ return null;
68
+ }
69
+
70
+ if (!content) return null;
71
+
72
+ logInfo(HOOK, `Found plan at: ${planPath}`);
73
+ logDebug(HOOK, `Plan length: ${content.length} chars`);
74
+
75
+ return {
76
+ path: planPath,
77
+ content,
78
+ hash: computePlanHash(content),
79
+ };
80
+ }