@pennixrv/trellis 0.6.45 → 0.7.0-beta.10

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 (92) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +21 -3
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/mem.d.ts.map +1 -1
  5. package/dist/commands/mem.js +6 -13
  6. package/dist/commands/mem.js.map +1 -1
  7. package/dist/commands/workflow.d.ts +17 -0
  8. package/dist/commands/workflow.d.ts.map +1 -1
  9. package/dist/commands/workflow.js +222 -0
  10. package/dist/commands/workflow.js.map +1 -1
  11. package/dist/configurators/claude.d.ts.map +1 -1
  12. package/dist/configurators/claude.js.map +1 -1
  13. package/dist/configurators/dsh.d.ts +16 -19
  14. package/dist/configurators/dsh.d.ts.map +1 -1
  15. package/dist/configurators/dsh.js +38 -26
  16. package/dist/configurators/dsh.js.map +1 -1
  17. package/dist/configurators/gemini.d.ts.map +1 -1
  18. package/dist/configurators/gemini.js.map +1 -1
  19. package/dist/configurators/opencode.d.ts.map +1 -1
  20. package/dist/configurators/opencode.js +4 -0
  21. package/dist/configurators/opencode.js.map +1 -1
  22. package/dist/migrations/manifests/0.6.17.json +2 -2
  23. package/dist/migrations/manifests/0.7.0-beta.10.json +9 -0
  24. package/dist/migrations/manifests/0.7.0-beta.4.json +9 -0
  25. package/dist/migrations/manifests/0.7.0-beta.4.pennix.1.json +9 -0
  26. package/dist/migrations/manifests/0.7.0-beta.5.json +9 -0
  27. package/dist/migrations/manifests/0.7.0-beta.6.json +9 -0
  28. package/dist/migrations/manifests/0.7.0-beta.7.json +9 -0
  29. package/dist/migrations/manifests/0.7.0-beta.8.json +9 -0
  30. package/dist/migrations/manifests/0.7.0-beta.9.json +9 -0
  31. package/dist/templates/claude/hooks/statusline.py +7 -0
  32. package/dist/templates/claude/settings.json +22 -0
  33. package/dist/templates/codex/hooks/session-start.py +20 -1
  34. package/dist/templates/codex/hooks.json +25 -0
  35. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +32 -3
  36. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  37. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +1 -0
  38. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +2 -1
  39. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +3 -2
  40. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  41. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +5 -4
  42. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -0
  43. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +2 -2
  44. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +3 -2
  45. package/dist/templates/common/commands/continue.md +4 -2
  46. package/dist/templates/common/skills/brainstorm.md +14 -6
  47. package/dist/templates/copilot/hooks/session-start.py +20 -1
  48. package/dist/templates/copilot/prompts/brainstorm.prompt.md +16 -6
  49. package/dist/templates/dsh/DSH.md +61 -37
  50. package/dist/templates/dsh/agents/trellis-check.md +99 -0
  51. package/dist/templates/dsh/agents/trellis-implement.md +106 -0
  52. package/dist/templates/dsh/agents/trellis-research.md +134 -0
  53. package/dist/templates/dsh/index.d.ts +12 -11
  54. package/dist/templates/dsh/index.d.ts.map +1 -1
  55. package/dist/templates/dsh/index.js +14 -12
  56. package/dist/templates/dsh/index.js.map +1 -1
  57. package/dist/templates/opencode/plugins/inject-spec-context.js +121 -0
  58. package/dist/templates/opencode/plugins/inject-workflow-state.js +111 -10
  59. package/dist/templates/pi/extensions/trellis/index.ts.txt +164 -10
  60. package/dist/templates/shared-hooks/index.d.ts +8 -2
  61. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  62. package/dist/templates/shared-hooks/index.js +13 -1
  63. package/dist/templates/shared-hooks/index.js.map +1 -1
  64. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
  65. package/dist/templates/shared-hooks/inject-spec-context.py +844 -0
  66. package/dist/templates/shared-hooks/inject-workflow-state.py +35 -9
  67. package/dist/templates/shared-hooks/session-start.py +22 -1
  68. package/dist/templates/trellis/agents/subnode.md +11 -4
  69. package/dist/templates/trellis/config.yaml +49 -0
  70. package/dist/templates/trellis/index.d.ts +3 -0
  71. package/dist/templates/trellis/index.d.ts.map +1 -1
  72. package/dist/templates/trellis/index.js +6 -0
  73. package/dist/templates/trellis/index.js.map +1 -1
  74. package/dist/templates/trellis/scripts/common/active_task.py +26 -4
  75. package/dist/templates/trellis/scripts/common/cli_adapter.py +38 -6
  76. package/dist/templates/trellis/scripts/common/config.py +18 -0
  77. package/dist/templates/trellis/scripts/common/context_projection.py +2 -2
  78. package/dist/templates/trellis/scripts/common/git_context.py +34 -2
  79. package/dist/templates/trellis/scripts/common/paths.py +28 -0
  80. package/dist/templates/trellis/scripts/common/spec_inject.py +439 -0
  81. package/dist/templates/trellis/scripts/common/spec_match.py +395 -0
  82. package/dist/templates/trellis/scripts/common/task_store.py +88 -1
  83. package/dist/templates/trellis/scripts/common/trellis_config.py +46 -7
  84. package/dist/templates/trellis/scripts/common/workflow_phase.py +3 -2
  85. package/dist/templates/trellis/scripts/common/workflow_selection.py +177 -0
  86. package/dist/templates/trellis/scripts/subnode_artifact.py +153 -15
  87. package/dist/templates/trellis/scripts/task.py +160 -0
  88. package/dist/templates/trellis/workflow.md +49 -34
  89. package/dist/types/ai-tools.d.ts.map +1 -1
  90. package/dist/types/ai-tools.js +26 -13
  91. package/dist/types/ai-tools.js.map +1 -1
  92. package/package.json +2 -2
@@ -1,4 +1,10 @@
1
- import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ readdirSync,
5
+ realpathSync,
6
+ statSync,
7
+ } from "node:fs";
2
8
  import { createHash, randomBytes } from "node:crypto";
3
9
  import {
4
10
  delimiter,
@@ -8,6 +14,7 @@ import {
8
14
  relative,
9
15
  resolve,
10
16
  } from "node:path";
17
+ import { homedir } from "node:os";
11
18
  import { spawn, spawnSync } from "node:child_process";
12
19
  import { isUtf8 } from "node:buffer";
13
20
 
@@ -773,10 +780,11 @@ function truncateUtf8(buf: Buffer, cap: number): Buffer {
773
780
  if ((lead & 0xe0) === 0xc0) seqLen = 2;
774
781
  else if ((lead & 0xf0) === 0xe0) seqLen = 3;
775
782
  else if ((lead & 0xf8) === 0xf0) seqLen = 4;
776
- // Drop the lead byte too if its full sequence didn't fit.
777
- if (i - 1 + seqLen > cap) i--;
783
+ // Cut before the lead byte when its full sequence didn't fit;
784
+ // otherwise the trailing sequence is complete — keep it whole.
785
+ if (i - 1 + seqLen > cap) return buf.subarray(0, i - 1);
778
786
  }
779
- return buf.subarray(0, i);
787
+ return buf.subarray(0, cap);
780
788
  }
781
789
 
782
790
  function stripInlineComment(value: string): string {
@@ -1115,10 +1123,66 @@ function readTaskDir(root: string, key: string | null): string | null {
1115
1123
  }
1116
1124
 
1117
1125
  // ── Workflow State Breadcrumb ─────────────────────────────────────────
1126
+ const WORKFLOW_ID_RE = /^[A-Za-z0-9_-]+$/;
1127
+ const DEFAULT_WORKFLOW_RE =
1128
+ /^default_workflow:\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/m;
1129
+
1130
+ function workflowVariant(root: string, workflowId: string): string {
1131
+ if (!WORKFLOW_ID_RE.test(workflowId)) return "";
1132
+ const path = join(root, ".trellis", "workflows", `${workflowId}.md`);
1133
+ return exists(path) ? path : "";
1134
+ }
1135
+
1136
+ function developerWorkflowId(root: string): string {
1137
+ for (const line of readText(join(root, ".trellis", ".developer")).split(
1138
+ /\r?\n/,
1139
+ )) {
1140
+ if (line.startsWith("workflow="))
1141
+ return line.slice("workflow=".length).trim();
1142
+ }
1143
+ return "";
1144
+ }
1145
+
1146
+ function configDefaultWorkflowId(root: string): string {
1147
+ return (
1148
+ readText(join(root, ".trellis", "config.yaml")).match(
1149
+ DEFAULT_WORKFLOW_RE,
1150
+ )?.[2] ?? ""
1151
+ );
1152
+ }
1153
+
1154
+ /** Mirrors common/workflow_selection.py without spawning another Python
1155
+ * process on every Pi turn. Invalid or missing selections fall through. */
1156
+ function resolveWorkflowMd(root: string, key: string | null): string {
1157
+ const taskDir = readTaskDir(root, key);
1158
+ let workflowId = "";
1159
+ if (taskDir) {
1160
+ try {
1161
+ const task = JSON.parse(
1162
+ readText(join(taskDir, "task.json")),
1163
+ ) as JsonObject;
1164
+ workflowId = typeof task.workflow === "string" ? task.workflow : "";
1165
+ } catch {}
1166
+ }
1167
+ if (workflowId) {
1168
+ const pinned = workflowVariant(root, workflowId);
1169
+ if (pinned) return pinned;
1170
+ console.error(
1171
+ `Warning: active task selects workflow ${JSON.stringify(workflowId)} but .trellis/workflows/ has no matching file; using default workflow resolution`,
1172
+ );
1173
+ }
1174
+
1175
+ const personal = workflowVariant(root, developerWorkflowId(root));
1176
+ if (personal) return personal;
1177
+ const team = workflowVariant(root, configDefaultWorkflowId(root));
1178
+ if (team) return team;
1179
+ return join(root, ".trellis", "workflow.md");
1180
+ }
1181
+
1118
1182
  const WF_RE =
1119
1183
  /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
1120
1184
  function workflowBreadcrumb(root: string, key: string | null): string {
1121
- const wf = readText(join(root, ".trellis", "workflow.md"));
1185
+ const wf = readText(resolveWorkflowMd(root, key));
1122
1186
  if (!wf) return "";
1123
1187
  const templates: Record<string, string> = {};
1124
1188
  for (const m of wf.matchAll(WF_RE)) {
@@ -1413,6 +1477,93 @@ function formatPiOutput(stdout: string, stderr: string): string {
1413
1477
  return ft || stdout || stderr;
1414
1478
  }
1415
1479
 
1480
+ // ── Permission-forwarding parent session (issue #610) ───────────
1481
+ // Headless `pi --mode json` children have no UI. `@gotgenes/pi-permission-system`
1482
+ // forwards `ask` to the parent only when the child env names the *serving*
1483
+ // parent session and is marked as a subagent. Three layers, all required:
1484
+ // 1. PI_SUBAGENT_PARENT_SESSION — out-of-process convention
1485
+ // 2. PI_SUBAGENT_CHILD=1 — stops pi-subagents overwriting that parent id
1486
+ // with the child's own session id
1487
+ // 3. serving heartbeat id, not stale PI_SESSION_ID (compaction can fork
1488
+ // the live id while the permission extension still listens on the
1489
+ // activation-time id). Drop inherited PI_SESSION_ID so the child
1490
+ // mints its own instead of colliding with the parent.
1491
+ let lastLiveSessionId: string | null = null;
1492
+
1493
+ function currentSessionId(ctx?: PiExtensionContext): string | null {
1494
+ const viaCtx = callStr(
1495
+ ctx?.sessionManager?.getSessionId,
1496
+ ctx?.sessionManager,
1497
+ );
1498
+ if (viaCtx) {
1499
+ lastLiveSessionId = viaCtx;
1500
+ return viaCtx;
1501
+ }
1502
+ return (
1503
+ lastLiveSessionId ??
1504
+ str(process.env.PI_SESSION_ID) ??
1505
+ str(process.env.PI_SESSIONID)
1506
+ );
1507
+ }
1508
+
1509
+ function piSessionsRoot(): string {
1510
+ const sessionDir = str(process.env.PI_CODING_AGENT_SESSION_DIR);
1511
+ if (sessionDir) return sessionDir;
1512
+ const agentDir = str(process.env.PI_CODING_AGENT_DIR);
1513
+ if (agentDir) return join(agentDir, "sessions");
1514
+ return join(homedir(), ".pi", "agent", "sessions");
1515
+ }
1516
+
1517
+ function servingSessionId(): string | null {
1518
+ try {
1519
+ const dir = join(piSessionsRoot(), "permission-forwarding", "serving");
1520
+ let best: { sessionId: string; updatedAt: number } | null = null;
1521
+ for (const name of readdirSync(dir)) {
1522
+ if (!name.endsWith(".json")) continue;
1523
+ let parsed: unknown;
1524
+ try {
1525
+ parsed = JSON.parse(readFileSync(join(dir, name), "utf-8"));
1526
+ } catch {
1527
+ continue;
1528
+ }
1529
+ if (!isObj(parsed) || parsed.pid !== process.pid) continue;
1530
+ const sessionId = str(parsed.sessionId);
1531
+ const updatedAt =
1532
+ typeof parsed.updatedAt === "number" && Number.isFinite(parsed.updatedAt)
1533
+ ? parsed.updatedAt
1534
+ : 0;
1535
+ if (!sessionId) continue;
1536
+ if (!best || updatedAt > best.updatedAt) best = { sessionId, updatedAt };
1537
+ }
1538
+ return best?.sessionId ?? null;
1539
+ } catch {
1540
+ return null;
1541
+ }
1542
+ }
1543
+
1544
+ function parentSessionIdForChild(ctx?: PiExtensionContext): string | null {
1545
+ return servingSessionId() ?? currentSessionId(ctx);
1546
+ }
1547
+
1548
+ function buildChildEnv(
1549
+ key?: string | null,
1550
+ parentSessionId?: string | null,
1551
+ ): NodeJS.ProcessEnv {
1552
+ const childEnv: NodeJS.ProcessEnv = {
1553
+ ...process.env,
1554
+ TRELLIS_SUBAGENT_CHILD: "1",
1555
+ // Stops pi-subagents from overwriting PI_SUBAGENT_PARENT_SESSION with the
1556
+ // child's own session id, and lets the permission system treat this process
1557
+ // as a subagent (ParentAuthorizer forwarding) even when no parent id is known.
1558
+ PI_SUBAGENT_CHILD: "1",
1559
+ ...(key ? { TRELLIS_CONTEXT_ID: key } : {}),
1560
+ ...(parentSessionId ? { PI_SUBAGENT_PARENT_SESSION: parentSessionId } : {}),
1561
+ };
1562
+ delete childEnv.PI_SESSION_ID;
1563
+ delete childEnv.PI_SESSIONID;
1564
+ return childEnv;
1565
+ }
1566
+
1416
1567
  // ── runPi: subprocess execution + event processing ───────────────────
1417
1568
  function runPi(
1418
1569
  root: string,
@@ -1422,6 +1573,7 @@ function runPi(
1422
1573
  emit: () => void,
1423
1574
  key?: string | null,
1424
1575
  signal?: AbortSignal,
1576
+ parentSessionId?: string | null,
1425
1577
  ): Promise<{ output: string; failed: boolean }> {
1426
1578
  return new Promise((resolve) => {
1427
1579
  if (signal?.aborted) {
@@ -1433,11 +1585,7 @@ function runPi(
1433
1585
  return;
1434
1586
  }
1435
1587
  const inv = resolvePiCli();
1436
- const childEnv = {
1437
- ...process.env,
1438
- TRELLIS_SUBAGENT_CHILD: "1",
1439
- ...(key ? { TRELLIS_CONTEXT_ID: key } : {}),
1440
- };
1588
+ const childEnv = buildChildEnv(key, parentSessionId);
1441
1589
  const cli = spawn(inv.command, [...inv.args, ...buildPiArgs(cfg)], {
1442
1590
  cwd: root,
1443
1591
  env: childEnv,
@@ -1538,7 +1686,9 @@ async function runSubagent(
1538
1686
  onUpdate?: (r: PiToolResult) => void,
1539
1687
  inheritedThinking?: string,
1540
1688
  inheritedModel?: string,
1689
+ ctx?: PiExtensionContext,
1541
1690
  ): Promise<{ output: string; details: ProgressDetails; failed: boolean }> {
1691
+ const parentSessionId = parentSessionIdForChild(ctx);
1542
1692
  const agentName = normalizeAgent(input.agent);
1543
1693
  const agentRaw = readText(join(root, ".pi", "agents", `${agentName}.md`));
1544
1694
  const agentCfg = parseAgentFM(agentRaw);
@@ -1606,6 +1756,7 @@ async function runSubagent(
1606
1756
  emit,
1607
1757
  key,
1608
1758
  signal,
1759
+ parentSessionId,
1609
1760
  ),
1610
1761
  ),
1611
1762
  );
@@ -1638,6 +1789,7 @@ async function runSubagent(
1638
1789
  emit,
1639
1790
  key,
1640
1791
  signal,
1792
+ parentSessionId,
1641
1793
  );
1642
1794
  prev = result.output;
1643
1795
  failed = failed || result.failed;
@@ -1657,6 +1809,7 @@ async function runSubagent(
1657
1809
  emit,
1658
1810
  key,
1659
1811
  signal,
1812
+ parentSessionId,
1660
1813
  );
1661
1814
  return finish(result.output, result.failed);
1662
1815
  } catch (e) {
@@ -1875,6 +2028,7 @@ export default function trellisExtension(pi: {
1875
2028
  onUpdate,
1876
2029
  inheritedThinking,
1877
2030
  inheritedModel,
2031
+ ctx,
1878
2032
  );
1879
2033
  return {
1880
2034
  content: [{ type: "text", text: result.output }],
@@ -11,8 +11,8 @@ export interface HookScript {
11
11
  /** Script content — no placeholders, ready to write directly */
12
12
  content: string;
13
13
  }
14
- export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py";
15
- export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro" | "trae" | "zcode";
14
+ export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py" | "inject-spec-context.py";
15
+ export type SharedHookPlatform = "claude" | "cursor" | "codex" | "opencode" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro" | "trae" | "zcode";
16
16
  /**
17
17
  * Which shared hooks each platform actually invokes. Single source of truth
18
18
  * for shared-hook distribution — `collectSharedHooks` reads this table, and
@@ -51,6 +51,12 @@ export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qod
51
51
  * (per-turn breadcrumb), and `inject-subagent-context.py` (sub-agent
52
52
  * spawn). The scripts emit a plain-text Kiro branch — Kiro adds a hook's
53
53
  * stdout directly to the conversation context (no JSON envelope).
54
+ * - `inject-spec-context.py` — path-scoped spec injection. Claude Code uses
55
+ * PostToolUse Read/Edit/Write/MultiEdit; Codex and OpenCode use PreToolUse.
56
+ * Codex passes the native apply_patch payload directly. OpenCode's plugin
57
+ * adapts write/edit/apply_patch calls and blocks a FULL emission once so the
58
+ * model sees the specs before retrying. Class-2 platforms use
59
+ * `get_context.py --mode spec` pull mode instead.
54
60
  * - Claude Code `statusLine` is intentionally not installed by default.
55
61
  * Users can add their own statusLine command in `.claude/settings.json`,
56
62
  * or opt in to the Trellis one via `trellis init --with-statusline`
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,CAAC;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CAoD1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,GAC5B,wBAAwB,CAAC;AAE7B,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,CAAC;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CA0D1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
@@ -51,6 +51,12 @@ function readTemplate(relativePath) {
51
51
  * (per-turn breadcrumb), and `inject-subagent-context.py` (sub-agent
52
52
  * spawn). The scripts emit a plain-text Kiro branch — Kiro adds a hook's
53
53
  * stdout directly to the conversation context (no JSON envelope).
54
+ * - `inject-spec-context.py` — path-scoped spec injection. Claude Code uses
55
+ * PostToolUse Read/Edit/Write/MultiEdit; Codex and OpenCode use PreToolUse.
56
+ * Codex passes the native apply_patch payload directly. OpenCode's plugin
57
+ * adapts write/edit/apply_patch calls and blocks a FULL emission once so the
58
+ * model sees the specs before retrying. Class-2 platforms use
59
+ * `get_context.py --mode spec` pull mode instead.
54
60
  * - Claude Code `statusLine` is intentionally not installed by default.
55
61
  * Users can add their own statusLine command in `.claude/settings.json`,
56
62
  * or opt in to the Trellis one via `trellis init --with-statusline`
@@ -67,13 +73,19 @@ export const SHARED_HOOKS_BY_PLATFORM = {
67
73
  "session-start.py",
68
74
  "inject-workflow-state.py",
69
75
  "inject-subagent-context.py",
76
+ "inject-spec-context.py",
70
77
  ],
71
78
  cursor: [
72
79
  "session-start.py",
73
80
  "inject-shell-session-context.py",
74
81
  "inject-subagent-context.py",
75
82
  ],
76
- codex: ["inject-workflow-state.py", "inject-subagent-context.py"],
83
+ codex: [
84
+ "inject-workflow-state.py",
85
+ "inject-subagent-context.py",
86
+ "inject-spec-context.py",
87
+ ],
88
+ opencode: ["inject-spec-context.py"],
77
89
  gemini: [
78
90
  "session-start.py",
79
91
  "inject-shell-session-context.py",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,4BAA4B;KAC7B;IACD,KAAK,EAAE,CAAC,0BAA0B,EAAE,4BAA4B,CAAC;IACjE,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;QAC5B,wBAAwB;KACzB;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,0BAA0B;QAC1B,4BAA4B;QAC5B,wBAAwB;KACzB;IACD,QAAQ,EAAE,CAAC,wBAAwB,CAAC;IACpC,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE;QACJ,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;KAC3B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
@@ -4,7 +4,7 @@
4
4
  No researched platform exports a session id into its shell tool's child
5
5
  process, but every hook-capable one puts that id on hook stdin. So the hook
6
6
  that fires just before a shell command writes a short-lived runtime ticket
7
- whenever the pending command calls `task.py start/current/finish`, and the
7
+ whenever the pending command calls `task.py start/replan/current/finish`, and the
8
8
  task script consumes it when it has no native session environment.
9
9
 
10
10
  Registered on whichever pre-shell event the host provides — Cursor's
@@ -35,7 +35,7 @@ if callable(_stdin_reconfigure):
35
35
  DIR_WORKFLOW = ".trellis"
36
36
  DIR_RUNTIME = ".runtime"
37
37
  DIR_SHELL_TICKETS = "shell-tickets"
38
- SESSION_SUBCOMMANDS = {"start", "current", "finish"}
38
+ SESSION_SUBCOMMANDS = {"start", "replan", "current", "finish"}
39
39
  TICKET_TTL_SECONDS = 30
40
40
  CONTEXT_IDENTITY_KEYS = (
41
41
  "session_id",
@@ -177,7 +177,7 @@ def _extract_task_subcommands(command: str) -> list[dict[str, str]]:
177
177
  if name not in SESSION_SUBCOMMANDS:
178
178
  continue
179
179
  item = {"name": name}
180
- if name == "start" and index + 2 < len(tokens):
180
+ if name in {"start", "replan"} and index + 2 < len(tokens):
181
181
  item["task_ref"] = tokens[index + 2]
182
182
  subcommands.append(item)
183
183
  return subcommands