opencode-empirical-plan 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-empirical-plan",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenCode lifecycle plugin for empirical plan orchestration.",
5
5
  "author": "chuck",
6
6
  "license": "MIT",
@@ -1,6 +1,25 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { type LifecycleState, STATE_FILENAME, validateLifecycleState } from "../schema.ts";
3
+ import { type LifecyclePhase, type LifecycleState, STATE_FILENAME, validateLifecycleState } from "../schema.ts";
4
+
5
+ const PHASE_ORDER: LifecyclePhase[] = ["plan", "execute", "reflect", "done"];
6
+
7
+ export async function assertPhaseAtLeast(
8
+ runDir: string,
9
+ required: LifecyclePhase,
10
+ ): Promise<{ ok: true } | { ok: false; reason: string }> {
11
+ const state = await readLifecycleState(runDir);
12
+ if (!state) return { ok: true };
13
+ const current = PHASE_ORDER.indexOf(state.phase);
14
+ const needed = PHASE_ORDER.indexOf(required);
15
+ if (current < needed) {
16
+ return {
17
+ ok: false,
18
+ reason: `lifecycle phase is "${state.phase}", expected at least "${required}"`,
19
+ };
20
+ }
21
+ return { ok: true };
22
+ }
4
23
 
5
24
  export async function readLifecycleState(runDir: string): Promise<LifecycleState | null> {
6
25
  const stateFile = path.join(runDir, STATE_FILENAME);
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { atomicWrite } from "../state/frontmatter.ts";
5
5
  import { ensureExecutionMd } from "../hooks/tool-after.ts";
6
+ import { assertPhaseAtLeast } from "../state/lifecycle-state.ts";
6
7
 
7
8
  const SUMMARY_MARKER = "<!-- EXECUTION:SUMMARY -->";
8
9
 
@@ -25,8 +26,9 @@ export const lifecycleRecordExecutionTool = tool({
25
26
  execution.md is the dedicated execution record file — separate from plan.md (which stays read-only after Phase 1-3).
26
27
 
27
28
  Guards (hard-block):
28
- - plan.md must exist and contain "## Plan Steps" (Phase 1-3 complete)
29
- - execution.md must already exist (initialized by the tool.execute.after hook when EXECUTE phase began)
29
+ - lifecycle-state.json phase must be >= "execute" (fail-open if state file missing)
30
+ - plan.md must exist
31
+ - execution.md must exist (auto-created if missing)
30
32
  - <!-- EXECUTION:SUMMARY --> must NOT already exist in execution.md (idempotent guard)
31
33
 
32
34
  On success: appends the execution_summary section into execution.md and updates its Phase frontmatter field to "summarized".
@@ -47,33 +49,33 @@ The execution_summary should compare each Plan Step against what actually happen
47
49
  const planFile = path.join(runDir, "plan.md");
48
50
  const executionFile = path.join(runDir, "execution.md");
49
51
 
50
- const result = await doRecord(planFile, executionFile, args.execution_summary);
52
+ const result = await doRecord(runDir, planFile, executionFile, args.execution_summary);
51
53
  return JSON.stringify(result);
52
54
  },
53
55
  });
54
56
 
55
57
  async function doRecord(
58
+ runDir: string,
56
59
  planFile: string,
57
60
  executionFile: string,
58
61
  executionSummary: string,
59
62
  ): Promise<RecordResult> {
60
- // Guard 1: plan.md must exist with Phase 1-3 complete
61
- if (!(await fileExists(planFile))) {
63
+ const phaseCheck = await assertPhaseAtLeast(runDir, "execute");
64
+ if (!phaseCheck.ok) {
62
65
  return {
63
66
  success: false,
64
67
  code: "PHASE_NOT_READY",
65
- reason: `plan.md not found at ${planFile}`,
68
+ reason: phaseCheck.reason,
66
69
  next_action: "Complete the PLAN phase first — run empirical-plan skill to produce plan.md",
67
70
  };
68
71
  }
69
72
 
70
- const planContent = await fs.readFile(planFile, "utf-8");
71
- if (!planContent.includes("## Plan Steps")) {
73
+ if (!(await fileExists(planFile))) {
72
74
  return {
73
75
  success: false,
74
76
  code: "PHASE_NOT_READY",
75
- reason: "plan.md does not contain '## Plan Steps' — Phase 1-3 incomplete",
76
- next_action: "Complete the PLAN phase using the empirical-plan skill",
77
+ reason: `plan.md not found at ${planFile}`,
78
+ next_action: "Complete the PLAN phase first run empirical-plan skill to produce plan.md",
77
79
  };
78
80
  }
79
81
 
@@ -2,8 +2,8 @@ import { tool } from "@opencode-ai/plugin";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { applyFrontmatterLifecycle, atomicWrite } from "../state/frontmatter.ts";
5
+ import { assertPhaseAtLeast } from "../state/lifecycle-state.ts";
5
6
 
6
- const EXECUTION_SUMMARY_MARKER = "<!-- EXECUTION:SUMMARY -->";
7
7
  const REFLECTION_MARKER = "<!-- LIFECYCLE:REFLECTION -->";
8
8
 
9
9
  async function fileExists(p: string): Promise<boolean> {
@@ -19,7 +19,8 @@ export const lifecycleRecordReflectionTool = tool({
19
19
  description: `Record Phase 5 (Reflection & Optimization) into plan.md.
20
20
 
21
21
  Guards (hard-block):
22
- - execution.md must exist and contain <!-- EXECUTION:SUMMARY --> (Phase 4 already recorded)
22
+ - lifecycle-state.json phase must be >= "reflect" (fail-open if state file missing)
23
+ - execution.md must exist
23
24
  - reflect.md must exist in run_dir (REFLECT phase complete)
24
25
  - <!-- LIFECYCLE:REFLECTION --> must NOT already exist in plan.md (idempotent guard)
25
26
 
@@ -40,7 +41,16 @@ The reflection_record should contain: execution quality assessment, plan design
40
41
  const executionFile = path.join(runDir, "execution.md");
41
42
  const reflectFile = path.join(runDir, "reflect.md");
42
43
 
43
- // Guard 1: plan.md must exist
44
+ const phaseCheck = await assertPhaseAtLeast(runDir, "reflect");
45
+ if (!phaseCheck.ok) {
46
+ return JSON.stringify({
47
+ success: false,
48
+ code: "PHASE_NOT_READY",
49
+ reason: phaseCheck.reason,
50
+ next_action: "Complete EXECUTE phase and call lifecycle_record_execution first",
51
+ });
52
+ }
53
+
44
54
  if (!(await fileExists(planFile))) {
45
55
  return JSON.stringify({
46
56
  success: false,
@@ -50,7 +60,7 @@ The reflection_record should contain: execution quality assessment, plan design
50
60
  });
51
61
  }
52
62
 
53
- // Guard 2: execution.md must exist and contain EXECUTION:SUMMARY
63
+ // Guard 2: execution.md must exist
54
64
  if (!(await fileExists(executionFile))) {
55
65
  return JSON.stringify({
56
66
  success: false,
@@ -60,16 +70,6 @@ The reflection_record should contain: execution quality assessment, plan design
60
70
  });
61
71
  }
62
72
 
63
- const executionContent = await fs.readFile(executionFile, "utf-8");
64
- if (!executionContent.includes(EXECUTION_SUMMARY_MARKER)) {
65
- return JSON.stringify({
66
- success: false,
67
- code: "EXECUTION_NOT_RECORDED",
68
- reason: "Phase 4 summary (EXECUTION:SUMMARY) not yet recorded in execution.md",
69
- next_action: "Call lifecycle_record_execution first to record Phase 4",
70
- });
71
- }
72
-
73
73
  // Guard 3: reflect.md must exist
74
74
  if (!(await fileExists(reflectFile))) {
75
75
  return JSON.stringify({