@opencode_weave/weave 0.5.2 → 0.6.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.
@@ -1,2 +1,2 @@
1
1
  import { type WeaveConfig } from "./schema";
2
- export declare function loadWeaveConfig(directory: string, _ctx?: unknown): WeaveConfig;
2
+ export declare function loadWeaveConfig(directory: string, _ctx?: unknown, _homeDir?: string): WeaveConfig;
@@ -78,10 +78,6 @@ export declare const TmuxConfigSchema: z.ZodObject<{
78
78
  }>>;
79
79
  main_pane_size: z.ZodOptional<z.ZodNumber>;
80
80
  }, z.core.$strip>;
81
- export declare const SkillsConfigSchema: z.ZodObject<{
82
- paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
83
- recursive: z.ZodOptional<z.ZodBoolean>;
84
- }, z.core.$strip>;
85
81
  export declare const ExperimentalConfigSchema: z.ZodObject<{
86
82
  plugin_load_timeout_ms: z.ZodOptional<z.ZodNumber>;
87
83
  context_window_warning_threshold: z.ZodOptional<z.ZodNumber>;
@@ -141,10 +137,6 @@ export declare const WeaveConfigSchema: z.ZodObject<{
141
137
  }>>;
142
138
  main_pane_size: z.ZodOptional<z.ZodNumber>;
143
139
  }, z.core.$strip>>;
144
- skills: z.ZodOptional<z.ZodObject<{
145
- paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
146
- recursive: z.ZodOptional<z.ZodBoolean>;
147
- }, z.core.$strip>>;
148
140
  experimental: z.ZodOptional<z.ZodObject<{
149
141
  plugin_load_timeout_ms: z.ZodOptional<z.ZodNumber>;
150
142
  context_window_warning_threshold: z.ZodOptional<z.ZodNumber>;
@@ -157,6 +149,5 @@ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>;
157
149
  export type CategoriesConfig = z.infer<typeof CategoriesConfigSchema>;
158
150
  export type BackgroundConfig = z.infer<typeof BackgroundConfigSchema>;
159
151
  export type TmuxConfig = z.infer<typeof TmuxConfigSchema>;
160
- export type SkillsConfig = z.infer<typeof SkillsConfigSchema>;
161
152
  export type ExperimentalConfig = z.infer<typeof ExperimentalConfigSchema>;
162
153
  export type WeaveConfig = z.infer<typeof WeaveConfigSchema>;
@@ -1,7 +1,6 @@
1
1
  export { loadSkills } from "./loader";
2
2
  export type { LoadSkillsOptions } from "./loader";
3
3
  export { createResolveSkillsFn, createSkillResolver, resolveMultipleSkills, resolveSkill } from "./resolver";
4
+ export { fetchSkillsFromOpenCode } from "./opencode-client";
4
5
  export { scanDirectory, parseFrontmatter } from "./discovery";
5
- export { mergeSkills } from "./merger";
6
- export { createBuiltinSkills } from "./builtin-skills";
7
6
  export type { SkillScope, SkillMetadata, LoadedSkill, SkillDiscoveryResult } from "./types";
@@ -1,6 +1,7 @@
1
1
  import type { SkillDiscoveryResult } from './types';
2
2
  export interface LoadSkillsOptions {
3
+ serverUrl: string | URL;
3
4
  directory?: string;
4
5
  disabledSkills?: string[];
5
6
  }
6
- export declare function loadSkills(options?: LoadSkillsOptions): SkillDiscoveryResult;
7
+ export declare function loadSkills(options: LoadSkillsOptions): Promise<SkillDiscoveryResult>;
@@ -0,0 +1,2 @@
1
+ import type { LoadedSkill } from "./types";
2
+ export declare function fetchSkillsFromOpenCode(serverUrl: string | URL, directory: string): Promise<LoadedSkill[]>;
@@ -4,19 +4,12 @@ export interface SkillMetadata {
4
4
  description?: string;
5
5
  model?: string;
6
6
  tools?: string | string[];
7
- mcp?: {
8
- name: string;
9
- type: "stdio" | "http";
10
- command?: string;
11
- args?: string[];
12
- url?: string;
13
- };
14
7
  }
15
8
  export interface LoadedSkill {
16
9
  name: string;
17
10
  description: string;
18
11
  content: string;
19
- scope: SkillScope;
12
+ scope?: SkillScope;
20
13
  path?: string;
21
14
  model?: string;
22
15
  }
@@ -1,5 +1,5 @@
1
1
  export type { WorkState, PlanProgress } from "./types";
2
2
  export type { ValidationResult, ValidationIssue, ValidationSeverity, ValidationCategory } from "./validation-types";
3
3
  export { WEAVE_DIR, WORK_STATE_FILE, WORK_STATE_PATH, PLANS_DIR } from "./constants";
4
- export { readWorkState, writeWorkState, clearWorkState, appendSessionId, createWorkState, findPlans, getPlanProgress, getPlanName, } from "./storage";
4
+ export { readWorkState, writeWorkState, clearWorkState, appendSessionId, createWorkState, findPlans, getPlanProgress, getPlanName, getHeadSha, } from "./storage";
5
5
  export { validatePlan } from "./validation";
@@ -21,7 +21,12 @@ export declare function appendSessionId(directory: string, sessionId: string): W
21
21
  /**
22
22
  * Create a fresh WorkState for a plan file.
23
23
  */
24
- export declare function createWorkState(planPath: string, sessionId: string, agent?: string): WorkState;
24
+ export declare function createWorkState(planPath: string, sessionId: string, agent?: string, directory?: string): WorkState;
25
+ /**
26
+ * Get the current HEAD SHA of the git repo at the given directory.
27
+ * Returns undefined if not a git repo or git is unavailable.
28
+ */
29
+ export declare function getHeadSha(directory: string): string | undefined;
25
30
  /**
26
31
  * Find all plan files in .weave/plans/, sorted by modification time (newest first).
27
32
  * Returns absolute paths.
@@ -13,6 +13,8 @@ export interface WorkState {
13
13
  plan_name: string;
14
14
  /** Agent type to use when resuming (e.g., "tapestry") */
15
15
  agent?: string;
16
+ /** Git HEAD SHA at the time work started (absent if not a git repo) */
17
+ start_sha?: string;
16
18
  }
17
19
  /**
18
20
  * Progress snapshot from counting checkboxes in a plan file.
package/dist/index.js CHANGED
@@ -46,10 +46,6 @@ var TmuxConfigSchema = z.object({
46
46
  layout: z.enum(["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]).optional(),
47
47
  main_pane_size: z.number().optional()
48
48
  });
49
- var SkillsConfigSchema = z.object({
50
- paths: z.array(z.string()).optional(),
51
- recursive: z.boolean().optional()
52
- });
53
49
  var ExperimentalConfigSchema = z.object({
54
50
  plugin_load_timeout_ms: z.number().min(1000).optional(),
55
51
  context_window_warning_threshold: z.number().min(0).max(1).optional(),
@@ -65,7 +61,6 @@ var WeaveConfigSchema = z.object({
65
61
  disabled_skills: z.array(z.string()).optional(),
66
62
  background: BackgroundConfigSchema.optional(),
67
63
  tmux: TmuxConfigSchema.optional(),
68
- skills: SkillsConfigSchema.optional(),
69
64
  experimental: ExperimentalConfigSchema.optional()
70
65
  });
71
66
 
@@ -100,7 +95,6 @@ function mergeConfigs(user, project) {
100
95
  disabled_skills: mergeStringArrays(user.disabled_skills, project.disabled_skills),
101
96
  background: project.background ?? user.background,
102
97
  tmux: project.tmux ?? user.tmux,
103
- skills: project.skills ?? user.skills,
104
98
  experimental: user.experimental || project.experimental ? { ...user.experimental, ...project.experimental } : undefined
105
99
  };
106
100
  }
@@ -152,8 +146,8 @@ function detectConfigFile(basePath) {
152
146
  return jsonPath;
153
147
  return null;
154
148
  }
155
- function loadWeaveConfig(directory, _ctx) {
156
- const userBasePath = join2(homedir(), ".config", "opencode", "weave-opencode");
149
+ function loadWeaveConfig(directory, _ctx, _homeDir) {
150
+ const userBasePath = join2(_homeDir ?? homedir(), ".config", "opencode", "weave-opencode");
157
151
  const projectBasePath = join2(directory, ".opencode", "weave-opencode");
158
152
  const userConfigPath = detectConfigFile(userBasePath);
159
153
  const projectConfigPath = detectConfigFile(projectBasePath);
@@ -573,15 +567,8 @@ For complex tasks that benefit from structured planning before execution:
573
567
  - /start-work loads the plan, creates work state at \`.weave/state.json\`, and switches to Tapestry
574
568
  - Tapestry reads the plan and works through tasks, marking checkboxes as it goes
575
569
  4. RESUME: If work was interrupted, \`/start-work\` resumes from the last unchecked task
576
- 5. POST-EXECUTION REVIEW (MANDATORY — NO SKIP CONDITIONS):
577
- After Tapestry reports all tasks complete, you MUST run this gate before reporting success to the user:
578
- a. Run \`git diff --stat\` to identify all changed files
579
- b. Delegate to Weft (quality review) AND Warp (security audit) in parallel
580
- c. Warp self-triages: if no security-relevant changes, it fast-exits with APPROVE — so always invoke it
581
- d. If Weft or Warp REJECT → address blocking issues, then re-run the rejecting reviewer
582
- e. Only report success to the user after BOTH Weft and Warp APPROVE
583
- - This step has NO skip conditions. Not for small changes, not for user request, not for time pressure.
584
- - Skipping this step is a workflow violation.
570
+
571
+ Note: Tapestry runs Weft and Warp reviews directly after completing all tasks Loom does not need to gate this.
585
572
 
586
573
  When to use this workflow vs. direct execution:
587
574
  - USE plan workflow: Large features, multi-file refactors, anything with 5+ steps or architectural decisions
@@ -591,11 +578,9 @@ When to use this workflow vs. direct execution:
591
578
  <ReviewWorkflow>
592
579
  Two review modes — different rules for each:
593
580
 
594
- **Post-Plan-Execution Review (after PlanWorkflow Step 5):**
595
- - ALWAYS mandatory. No skip conditions. See PlanWorkflow Step 5 for the full protocol.
596
- - ALWAYS delegate to BOTH Weft (quality) AND Warp (security) in parallel
597
- - Warp self-triages: fast-exits with APPROVE if no security-relevant changes detected
598
- - Both must APPROVE before reporting success to the user
581
+ **Post-Plan-Execution Review:**
582
+ - Handled directly by Tapestry Tapestry invokes Weft and Warp after completing all tasks.
583
+ - Loom does not need to intervene.
599
584
 
600
585
  **Ad-Hoc Review (non-plan work):**
601
586
  - Delegate to Weft to review the changes
@@ -642,7 +627,6 @@ var TAPESTRY_DEFAULTS = {
642
627
  temperature: 0.1,
643
628
  description: "Tapestry (Execution Orchestrator)",
644
629
  tools: {
645
- task: false,
646
630
  call_weave_agent: false
647
631
  },
648
632
  prompt: `<Role>
@@ -700,16 +684,51 @@ When activated by /start-work with a plan file:
700
684
  3. For each task:
701
685
  a. Read the task description, files, and acceptance criteria
702
686
  b. Execute the work (write code, run commands, create files)
703
- c. Verify: Read changed files, run tests, check acceptance criteria. If uncertain about quality, note that Loom should invoke Weft for formal review.
687
+ c. Verify: Follow the <Verification> protocol below ALL checks must pass before marking complete. If uncertain about quality, note that Loom should invoke Weft for formal review.
704
688
  d. Mark complete: use Edit tool to change \`- [ ]\` to \`- [x]\` in the plan file
705
689
  e. Report: "Completed task N/M: [title]"
706
690
  4. CONTINUE to the next unchecked task
707
- 5. When ALL checkboxes are checked, report final summary and include:
708
- "All tasks complete. **Post-execution review required** — Loom must run Weft and Warp before reporting success."
691
+ 5. When ALL checkboxes are checked, follow the <PostExecutionReview> protocol below before reporting final summary.
709
692
 
710
693
  NEVER stop mid-plan unless explicitly told to or completely blocked.
711
694
  </PlanExecution>
712
695
 
696
+ <Verification>
697
+ After completing work for each task — BEFORE marking \`- [ ]\` → \`- [x]\`:
698
+
699
+ 1. **Inspect changes**:
700
+ - Review your Edit/Write tool call history to identify all files you modified
701
+ - Read EVERY changed file to confirm correctness
702
+ - Cross-check: does the code actually implement what the task required?
703
+
704
+ 2. **Validate acceptance criteria**:
705
+ - Re-read the task's acceptance criteria from the plan
706
+ - Verify EACH criterion is met — exactly, not approximately
707
+ - If any criterion is unmet: address it, then re-verify
708
+
709
+ 3. **Accumulate learnings** (if \`.weave/learnings/{plan-name}.md\` exists or plan has multiple tasks):
710
+ - After verification passes, append 1-3 bullet points of key findings
711
+ - Before starting the NEXT task, read the learnings file for context from previous tasks
712
+
713
+ **Gate**: Only mark complete when ALL checks pass. If ANY check fails, fix first.
714
+ </Verification>
715
+
716
+ <PostExecutionReview>
717
+ After ALL plan tasks are checked off, run this mandatory review gate:
718
+
719
+ 1. Identify all changed files:
720
+ - If a **Start SHA** was provided in the session context, run \`git diff --name-only <start-sha>..HEAD\` to get the complete list of changed files (this captures all changes including intermediate commits)
721
+ - If no Start SHA is available (non-git workspace), use the plan's \`**Files**:\` fields as the review scope
722
+ 2. Delegate to Weft (quality review) AND Warp (security audit) in parallel using the Task tool:
723
+ - Weft: subagent_type "weft" — reviews code quality
724
+ - Warp: subagent_type "warp" — audits security (self-triages; fast-exits with APPROVE if no security-relevant changes)
725
+ - Include the list of changed files in your prompt to each reviewer
726
+ 3. Report the review results to the user:
727
+ - Summarize Weft's and Warp's findings (APPROVE or REJECT with details)
728
+ - If either reviewer REJECTS, present the blocking issues to the user for decision — do NOT attempt to fix them yourself
729
+ - Tapestry follows the plan; review findings require user approval before any further changes
730
+ </PostExecutionReview>
731
+
713
732
  <Execution>
714
733
  - Work through tasks top to bottom
715
734
  - Verify each step before marking complete
@@ -1378,6 +1397,14 @@ function createBuiltinAgents(options = {}) {
1378
1397
  resolveSkills
1379
1398
  });
1380
1399
  if (override) {
1400
+ if (override.skills?.length && resolveSkills) {
1401
+ const skillContent = resolveSkills(override.skills, disabledSkills);
1402
+ if (skillContent) {
1403
+ built.prompt = skillContent + (built.prompt ? `
1404
+
1405
+ ` + built.prompt : "");
1406
+ }
1407
+ }
1381
1408
  if (override.prompt_append) {
1382
1409
  built.prompt = (built.prompt ? built.prompt + `
1383
1410
 
@@ -1412,6 +1439,55 @@ function createManagers(options) {
1412
1439
  import * as path3 from "path";
1413
1440
  import * as os2 from "os";
1414
1441
 
1442
+ // src/features/skill-loader/opencode-client.ts
1443
+ function deriveScope(location) {
1444
+ if (location.includes(".opencode"))
1445
+ return "project";
1446
+ return "user";
1447
+ }
1448
+ async function fetchSkillsFromOpenCode(serverUrl, directory) {
1449
+ const base = serverUrl.toString().replace(/\/$/, "");
1450
+ const url = `${base}/skill?directory=${encodeURIComponent(directory)}`;
1451
+ let response;
1452
+ try {
1453
+ response = await fetch(url, { signal: AbortSignal.timeout(3000) });
1454
+ } catch (err) {
1455
+ log("Failed to fetch skills from OpenCode — skills will not be loaded", { url, error: String(err) });
1456
+ return [];
1457
+ }
1458
+ if (!response.ok) {
1459
+ log("OpenCode /skill endpoint returned non-OK status — skills will not be loaded", {
1460
+ url,
1461
+ status: response.status
1462
+ });
1463
+ return [];
1464
+ }
1465
+ let data;
1466
+ try {
1467
+ data = await response.json();
1468
+ } catch (err) {
1469
+ log("Failed to parse skills response from OpenCode", { url, error: String(err) });
1470
+ return [];
1471
+ }
1472
+ if (!Array.isArray(data)) {
1473
+ log("Unexpected skills response shape from OpenCode — expected array", { url });
1474
+ return [];
1475
+ }
1476
+ const skills = [];
1477
+ for (const item of data) {
1478
+ if (!item.name)
1479
+ continue;
1480
+ skills.push({
1481
+ name: item.name,
1482
+ description: item.description ?? "",
1483
+ content: item.content ?? "",
1484
+ scope: deriveScope(item.location ?? ""),
1485
+ path: item.location
1486
+ });
1487
+ }
1488
+ return skills;
1489
+ }
1490
+
1415
1491
  // src/features/skill-loader/discovery.ts
1416
1492
  import * as fs2 from "fs";
1417
1493
  import * as path2 from "path";
@@ -1535,38 +1611,40 @@ function loadSkillFile(filePath, scope) {
1535
1611
  return { name: metadata.name, description: metadata.description ?? "", content, scope, path: filePath, model: metadata.model };
1536
1612
  }
1537
1613
 
1538
- // src/features/skill-loader/merger.ts
1539
- var SCOPE_PRIORITY = { project: 3, user: 2, builtin: 1 };
1540
- function mergeSkills(skills) {
1541
- const skillMap = new Map;
1542
- for (const skill of skills) {
1543
- const existing = skillMap.get(skill.name);
1544
- if (!existing || (SCOPE_PRIORITY[skill.scope] ?? 0) > (SCOPE_PRIORITY[existing.scope] ?? 0)) {
1545
- skillMap.set(skill.name, skill);
1546
- }
1547
- }
1548
- return Array.from(skillMap.values());
1549
- }
1550
-
1551
- // src/features/skill-loader/builtin-skills.ts
1552
- function createBuiltinSkills() {
1553
- return [];
1554
- }
1555
-
1556
1614
  // src/features/skill-loader/loader.ts
1557
- function loadSkills(options = {}) {
1558
- const { directory, disabledSkills = [] } = options;
1559
- const projectDir = path3.join(directory ?? process.cwd(), ".opencode", "skills");
1560
- const userDir = path3.join(os2.homedir(), ".config", "opencode", "weave-opencode", "skills");
1561
- const projectSkills = scanDirectory({ directory: projectDir, scope: "project" });
1615
+ function scanFilesystemSkills(directory) {
1616
+ const userDir = path3.join(os2.homedir(), ".config", "opencode", "skills");
1617
+ const projectDir = path3.join(directory, ".opencode", "skills");
1562
1618
  const userSkills = scanDirectory({ directory: userDir, scope: "user" });
1563
- const builtinSkills = createBuiltinSkills();
1564
- const all = [...projectSkills, ...userSkills, ...builtinSkills];
1565
- const merged = mergeSkills(all);
1619
+ const projectSkills = scanDirectory({ directory: projectDir, scope: "project" });
1620
+ return [...projectSkills, ...userSkills];
1621
+ }
1622
+ function mergeSkillSources(apiSkills, fsSkills) {
1623
+ const seen = new Set(apiSkills.map((s) => s.name));
1624
+ const merged = [...apiSkills];
1625
+ for (const skill of fsSkills) {
1626
+ if (!seen.has(skill.name)) {
1627
+ merged.push(skill);
1628
+ seen.add(skill.name);
1629
+ }
1630
+ }
1631
+ return merged;
1632
+ }
1633
+ async function loadSkills(options) {
1634
+ const { serverUrl, directory = process.cwd(), disabledSkills = [] } = options;
1635
+ const apiSkills = await fetchSkillsFromOpenCode(serverUrl, directory);
1636
+ const fsSkills = scanFilesystemSkills(directory);
1637
+ const skills = mergeSkillSources(apiSkills, fsSkills);
1638
+ if (apiSkills.length === 0 && fsSkills.length > 0) {
1639
+ log("OpenCode API returned no skills — using filesystem fallback", {
1640
+ fsSkillCount: fsSkills.length,
1641
+ fsSkillNames: fsSkills.map((s) => s.name)
1642
+ });
1643
+ }
1566
1644
  if (disabledSkills.length === 0)
1567
- return { skills: merged };
1645
+ return { skills };
1568
1646
  const disabledSet = new Set(disabledSkills);
1569
- return { skills: merged.filter((s) => !disabledSet.has(s.name)) };
1647
+ return { skills: skills.filter((s) => !disabledSet.has(s.name)) };
1570
1648
  }
1571
1649
  // src/features/skill-loader/resolver.ts
1572
1650
  function resolveSkill(name, result) {
@@ -1596,7 +1674,8 @@ function createSkillResolver(discovered) {
1596
1674
  // src/create-tools.ts
1597
1675
  async function createTools(options) {
1598
1676
  const { ctx, pluginConfig } = options;
1599
- const skillResult = loadSkills({
1677
+ const skillResult = await loadSkills({
1678
+ serverUrl: ctx.serverUrl,
1600
1679
  directory: ctx.directory,
1601
1680
  disabledSkills: pluginConfig.disabled_skills ?? []
1602
1681
  });
@@ -1796,6 +1875,7 @@ var PLANS_DIR = `${WEAVE_DIR}/plans`;
1796
1875
  // src/features/work-state/storage.ts
1797
1876
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync, unlinkSync, mkdirSync, readdirSync as readdirSync2, statSync } from "fs";
1798
1877
  import { join as join6, basename } from "path";
1878
+ import { execSync } from "child_process";
1799
1879
  var UNCHECKED_RE = /^[-*]\s*\[\s*\]/gm;
1800
1880
  var CHECKED_RE = /^[-*]\s*\[[xX]\]/gm;
1801
1881
  function readWorkState(directory) {
@@ -1850,15 +1930,29 @@ function appendSessionId(directory, sessionId) {
1850
1930
  }
1851
1931
  return state;
1852
1932
  }
1853
- function createWorkState(planPath, sessionId, agent) {
1933
+ function createWorkState(planPath, sessionId, agent, directory) {
1934
+ const startSha = directory ? getHeadSha(directory) : undefined;
1854
1935
  return {
1855
1936
  active_plan: planPath,
1856
1937
  started_at: new Date().toISOString(),
1857
1938
  session_ids: [sessionId],
1858
1939
  plan_name: getPlanName(planPath),
1859
- ...agent !== undefined ? { agent } : {}
1940
+ ...agent !== undefined ? { agent } : {},
1941
+ ...startSha !== undefined ? { start_sha: startSha } : {}
1860
1942
  };
1861
1943
  }
1944
+ function getHeadSha(directory) {
1945
+ try {
1946
+ const sha = execSync("git rev-parse HEAD", {
1947
+ cwd: directory,
1948
+ encoding: "utf-8",
1949
+ stdio: ["pipe", "pipe", "pipe"]
1950
+ }).trim();
1951
+ return sha || undefined;
1952
+ } catch {
1953
+ return;
1954
+ }
1955
+ }
1862
1956
  function findPlans(directory) {
1863
1957
  const plansDir = join6(directory, PLANS_DIR);
1864
1958
  try {
@@ -2205,7 +2299,7 @@ Tell the user to fix the plan file and run /start-work again.`
2205
2299
  };
2206
2300
  }
2207
2301
  appendSessionId(directory, sessionId);
2208
- const resumeContext = buildResumeContext(existingState.active_plan, existingState.plan_name, progress);
2302
+ const resumeContext = buildResumeContext(existingState.active_plan, existingState.plan_name, progress, existingState.start_sha);
2209
2303
  if (validation.warnings.length > 0) {
2210
2304
  return {
2211
2305
  switchAgent: "tapestry",
@@ -2269,9 +2363,9 @@ Tell the user to fix these issues in the plan file and try again.`
2269
2363
  };
2270
2364
  }
2271
2365
  clearWorkState(directory);
2272
- const state = createWorkState(matched, sessionId, "tapestry");
2366
+ const state = createWorkState(matched, sessionId, "tapestry", directory);
2273
2367
  writeWorkState(directory, state);
2274
- const freshContext = buildFreshContext(matched, getPlanName(matched), progress);
2368
+ const freshContext = buildFreshContext(matched, getPlanName(matched), progress, state.start_sha);
2275
2369
  if (validation.warnings.length > 0) {
2276
2370
  return {
2277
2371
  switchAgent: "tapestry",
@@ -2317,9 +2411,9 @@ ${formatValidationResults(validation)}
2317
2411
  Tell the user to fix these issues in the plan file and try again.`
2318
2412
  };
2319
2413
  }
2320
- const state = createWorkState(plan, sessionId, "tapestry");
2414
+ const state = createWorkState(plan, sessionId, "tapestry", directory);
2321
2415
  writeWorkState(directory, state);
2322
- const freshContext = buildFreshContext(plan, getPlanName(plan), progress);
2416
+ const freshContext = buildFreshContext(plan, getPlanName(plan), progress, state.start_sha);
2323
2417
  if (validation.warnings.length > 0) {
2324
2418
  return {
2325
2419
  switchAgent: "tapestry",
@@ -2375,10 +2469,12 @@ function formatValidationResults(result) {
2375
2469
  return lines.join(`
2376
2470
  `);
2377
2471
  }
2378
- function buildFreshContext(planPath, planName, progress) {
2472
+ function buildFreshContext(planPath, planName, progress, startSha) {
2473
+ const shaLine = startSha ? `
2474
+ **Start SHA**: ${startSha}` : "";
2379
2475
  return `## Starting Plan: ${planName}
2380
2476
  **Plan file**: ${planPath}
2381
- **Progress**: ${progress.completed}/${progress.total} tasks completed
2477
+ **Progress**: ${progress.completed}/${progress.total} tasks completed${shaLine}
2382
2478
 
2383
2479
  Read the plan file now and begin executing from the first unchecked \`- [ ]\` task.
2384
2480
 
@@ -2389,12 +2485,14 @@ Before starting any work, use todowrite to populate the sidebar:
2389
2485
  3. Create todos for the next 2-3 tasks (pending)
2390
2486
  Keep each todo under 35 chars. Update as you complete tasks.`;
2391
2487
  }
2392
- function buildResumeContext(planPath, planName, progress) {
2488
+ function buildResumeContext(planPath, planName, progress, startSha) {
2393
2489
  const remaining = progress.total - progress.completed;
2490
+ const shaLine = startSha ? `
2491
+ **Start SHA**: ${startSha}` : "";
2394
2492
  return `## Resuming Plan: ${planName}
2395
2493
  **Plan file**: ${planPath}
2396
2494
  **Progress**: ${progress.completed}/${progress.total} tasks completed
2397
- **Status**: RESUMING — continuing from where the previous session left off.
2495
+ **Status**: RESUMING — continuing from where the previous session left off.${shaLine}
2398
2496
 
2399
2497
  Read the plan file now and continue from the first unchecked \`- [ ]\` task.
2400
2498
 
@@ -2508,6 +2606,7 @@ function clearSession2(sessionId) {
2508
2606
  // src/plugin/plugin-interface.ts
2509
2607
  function createPluginInterface(args) {
2510
2608
  const { pluginConfig, hooks, tools, configHandler, agents, client } = args;
2609
+ let pendingInterrupt = false;
2511
2610
  return {
2512
2611
  tool: tools,
2513
2612
  config: async (config) => {
@@ -2613,25 +2712,37 @@ ${result.contextInjection}`;
2613
2712
  }
2614
2713
  }
2615
2714
  }
2616
- if (hooks.workContinuation && event.type === "session.idle") {
2715
+ if (event.type === "tui.command.execute") {
2617
2716
  const evt = event;
2618
- const sessionId = evt.properties?.sessionID ?? "";
2619
- if (sessionId) {
2620
- const result = hooks.workContinuation(sessionId);
2621
- if (result.continuationPrompt && client) {
2622
- try {
2623
- await client.session.promptAsync({
2624
- path: { id: sessionId },
2625
- body: {
2626
- parts: [{ type: "text", text: result.continuationPrompt }]
2627
- }
2628
- });
2629
- log("[work-continuation] Injected continuation prompt", { sessionId });
2630
- } catch (err) {
2631
- log("[work-continuation] Failed to inject continuation", { sessionId, error: String(err) });
2717
+ if (evt.properties?.command === "session.interrupt") {
2718
+ pendingInterrupt = true;
2719
+ log("[work-continuation] User interrupt detected — will suppress next continuation");
2720
+ }
2721
+ }
2722
+ if (hooks.workContinuation && event.type === "session.idle") {
2723
+ if (pendingInterrupt) {
2724
+ pendingInterrupt = false;
2725
+ log("[work-continuation] Skipping continuation session was interrupted by user");
2726
+ } else {
2727
+ const evt = event;
2728
+ const sessionId = evt.properties?.sessionID ?? "";
2729
+ if (sessionId) {
2730
+ const result = hooks.workContinuation(sessionId);
2731
+ if (result.continuationPrompt && client) {
2732
+ try {
2733
+ await client.session.promptAsync({
2734
+ path: { id: sessionId },
2735
+ body: {
2736
+ parts: [{ type: "text", text: result.continuationPrompt }]
2737
+ }
2738
+ });
2739
+ log("[work-continuation] Injected continuation prompt", { sessionId });
2740
+ } catch (err) {
2741
+ log("[work-continuation] Failed to inject continuation", { sessionId, error: String(err) });
2742
+ }
2743
+ } else if (result.continuationPrompt) {
2744
+ log("[work-continuation] continuationPrompt available but no client", { sessionId });
2632
2745
  }
2633
- } else if (result.continuationPrompt) {
2634
- log("[work-continuation] continuationPrompt available but no client", { sessionId });
2635
2746
  }
2636
2747
  }
2637
2748
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode_weave/weave",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Weave — lean OpenCode plugin with multi-agent orchestration",
5
5
  "author": "Weave",
6
6
  "license": "MIT",
@@ -1,2 +0,0 @@
1
- import type { LoadedSkill } from "./types";
2
- export declare function createBuiltinSkills(): LoadedSkill[];
@@ -1,2 +0,0 @@
1
- import type { LoadedSkill } from './types';
2
- export declare function mergeSkills(skills: LoadedSkill[]): LoadedSkill[];