supipowers 2.0.2 → 2.2.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.
Files changed (84) hide show
  1. package/README.md +5 -6
  2. package/package.json +4 -2
  3. package/skills/harness/SKILL.md +1 -0
  4. package/src/bootstrap.ts +8 -133
  5. package/src/commands/optimize-context.ts +153 -16
  6. package/src/commands/runbook.ts +511 -0
  7. package/src/config/defaults.ts +5 -5
  8. package/src/config/loader.ts +1 -0
  9. package/src/config/schema.ts +2 -6
  10. package/src/context/rule-renderer.ts +274 -2
  11. package/src/context/runbook-extension-template.ts +193 -0
  12. package/src/context/startup-check.ts +197 -2
  13. package/src/context/startup-optimizer.ts +133 -10
  14. package/src/context-mode/knowledge/store.ts +381 -43
  15. package/src/context-mode/tools.ts +41 -3
  16. package/src/deps/registry.ts +1 -12
  17. package/src/fix-pr/assessment.ts +1 -0
  18. package/src/fix-pr/prompt-builder.ts +1 -0
  19. package/src/git/commit.ts +76 -18
  20. package/src/harness/command.ts +201 -12
  21. package/src/harness/default-agents/docs.md +39 -0
  22. package/src/harness/docs/config.ts +29 -0
  23. package/src/harness/docs/glob-match.ts +27 -0
  24. package/src/harness/docs/index-renderer.ts +82 -0
  25. package/src/harness/docs/provenance.ts +125 -0
  26. package/src/harness/docs/regen-decision.ts +167 -0
  27. package/src/harness/docs/representative-files.ts +175 -0
  28. package/src/harness/docs/source-hash.ts +106 -0
  29. package/src/harness/docs/validator.ts +233 -0
  30. package/src/harness/git-verification.ts +515 -0
  31. package/src/harness/git-verify-qa.ts +406 -0
  32. package/src/harness/hooks/layer-context-inject.ts +35 -1
  33. package/src/harness/hooks/register.ts +24 -3
  34. package/src/harness/pipeline.ts +37 -13
  35. package/src/harness/pr-comment/baseline.ts +105 -0
  36. package/src/harness/pr-comment/ci-env.ts +120 -0
  37. package/src/harness/pr-comment/gh-poster.ts +227 -0
  38. package/src/harness/pr-comment/handler.ts +198 -0
  39. package/src/harness/pr-comment/render.ts +297 -0
  40. package/src/harness/pr-comment/status.ts +95 -0
  41. package/src/harness/pr-comment/types.ts +73 -0
  42. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  43. package/src/harness/project-paths.ts +95 -0
  44. package/src/harness/stages/design.ts +1 -0
  45. package/src/harness/stages/discover.ts +1 -13
  46. package/src/harness/stages/docs.ts +708 -0
  47. package/src/harness/stages/implement-apply.ts +934 -0
  48. package/src/harness/stages/implement.ts +64 -51
  49. package/src/harness/stages/plan.ts +25 -16
  50. package/src/harness/stages/validate.ts +478 -0
  51. package/src/harness/storage.ts +142 -0
  52. package/src/harness/tools.ts +130 -0
  53. package/src/mempalace/bridge.ts +207 -41
  54. package/src/mempalace/config.ts +10 -4
  55. package/src/mempalace/format.ts +122 -6
  56. package/src/mempalace/hooks.ts +204 -56
  57. package/src/mempalace/installer-helper.ts +18 -4
  58. package/src/mempalace/python/mempalace_bridge.py +128 -3
  59. package/src/mempalace/runtime.ts +53 -16
  60. package/src/mempalace/schema.ts +151 -30
  61. package/src/mempalace/session-summary.ts +5 -0
  62. package/src/mempalace/tool.ts +17 -4
  63. package/src/mempalace/upstream-limits.ts +69 -0
  64. package/src/planning/approval-flow.ts +25 -2
  65. package/src/planning/planning-ask-tool.ts +34 -4
  66. package/src/planning/system-prompt.ts +1 -1
  67. package/src/tool-catalog/active-tool-controller.ts +0 -22
  68. package/src/tool-catalog/active-tool-planner.ts +0 -26
  69. package/src/tool-catalog/tool-groups.ts +1 -9
  70. package/src/types.ts +127 -8
  71. package/src/ui-design/session.ts +114 -8
  72. package/src/utils/executable.ts +10 -1
  73. package/src/workspace/state-paths.ts +1 -1
  74. package/src/commands/mcp.ts +0 -814
  75. package/src/mcp/activation.ts +0 -77
  76. package/src/mcp/config.ts +0 -223
  77. package/src/mcp/docs.ts +0 -154
  78. package/src/mcp/gateway.ts +0 -103
  79. package/src/mcp/lifecycle.ts +0 -79
  80. package/src/mcp/manager-tool.ts +0 -104
  81. package/src/mcp/mcpc.ts +0 -113
  82. package/src/mcp/registry.ts +0 -98
  83. package/src/mcp/triggers.ts +0 -62
  84. package/src/mcp/types.ts +0 -95
@@ -4,17 +4,11 @@ import { getMetricsStore, getSessionId } from "../context-mode/hooks.js";
4
4
  import { getProjectStateDir } from "../workspace/state-paths.js";
5
5
  import type { Platform } from "../platform/types.js";
6
6
  import { normalizeSystemPromptBlocks, systemPromptText } from "../platform/system-prompt.js";
7
- import type { McpRegistry } from "../mcp/types.js";
8
7
  import type { SupipowersConfig } from "../types.js";
9
8
  import { planActiveTools } from "./active-tool-planner.js";
10
9
  import { detectContextMode } from "../context-mode/detector.js";
11
10
  import { getShadowedNativeTools } from "../context-mode/routing.js";
12
11
 
13
- export interface ActiveToolControllerDeps {
14
- loadMcpRegistryForCwd(cwd: string): McpRegistry;
15
- consumePendingTags(): string[];
16
- }
17
-
18
12
  type BeforeAgentStartEventLike = {
19
13
  prompt?: string;
20
14
  systemPrompt?: string | string[];
@@ -33,7 +27,6 @@ type BeforeAgentStartContextLike = {
33
27
  export function registerActiveToolController(
34
28
  platform: Platform,
35
29
  config: SupipowersConfig,
36
- _deps: ActiveToolControllerDeps,
37
30
  ): void {
38
31
  if (!config.contextMode.enabled || !config.contextMode.lazyTools.enabled) return;
39
32
 
@@ -46,19 +39,6 @@ export function registerActiveToolController(
46
39
  if (typeof ctx.getSystemPrompt !== "function") return undefined;
47
40
 
48
41
  const cwd = typeof ctx.cwd === "string" && ctx.cwd.length > 0 ? ctx.cwd : process.cwd();
49
- let registry: McpRegistry = { schemaVersion: 1, servers: {} };
50
- try {
51
- registry = _deps.loadMcpRegistryForCwd(cwd);
52
- } catch (error) {
53
- (platform as any).logger?.warn?.("supi-lazy-tools: failed to load MCP registry", error);
54
- }
55
-
56
- let pendingTags: string[] = [];
57
- try {
58
- pendingTags = _deps.consumePendingTags();
59
- } catch (error) {
60
- (platform as any).logger?.warn?.("supi-lazy-tools: failed to consume MCP tags", error);
61
- }
62
42
 
63
43
  let plan;
64
44
  try {
@@ -67,8 +47,6 @@ export function registerActiveToolController(
67
47
  currentActive: platform.getActiveTools(),
68
48
  allTools: platform.getAllTools(),
69
49
  lazyTools: config.contextMode.lazyTools,
70
- mcpServers: registry.servers,
71
- pendingTags,
72
50
  cacheHandlesEnabled: config.contextMode.cacheHandles.enabled,
73
51
  });
74
52
  } catch (error) {
@@ -1,10 +1,7 @@
1
- import { computeActiveServers } from "../mcp/activation.js";
2
- import type { ServerConfig } from "../mcp/types.js";
3
1
  import type { ContextModeLazyToolsConfig } from "../types.js";
4
2
  import {
5
3
  BALANCED_KEYWORD_TOOLS,
6
4
  CONTEXT_MODE_TOOL_NAMES,
7
- MCPC_MANAGER_TOOL_NAME,
8
5
  isSupiOwnedTool,
9
6
  orderOwnedTools,
10
7
  } from "./tool-groups.js";
@@ -12,8 +9,6 @@ import {
12
9
  export interface ActiveToolPlannerDiagnostics {
13
10
  unknownConfiguredTools: string[];
14
11
  unavailableTools: string[];
15
- unmatchedTags: string[];
16
- missingMcpGatewayTools: string[];
17
12
  }
18
13
 
19
14
  export interface PlanActiveToolsInput {
@@ -21,8 +16,6 @@ export interface PlanActiveToolsInput {
21
16
  currentActive: string[];
22
17
  allTools: string[];
23
18
  lazyTools: ContextModeLazyToolsConfig;
24
- mcpServers?: Record<string, ServerConfig>;
25
- pendingTags?: string[];
26
19
  cacheHandlesEnabled?: boolean;
27
20
  }
28
21
 
@@ -41,8 +34,6 @@ export function planActiveTools(input: PlanActiveToolsInput): ActiveToolPlan {
41
34
  const diagnostics: ActiveToolPlannerDiagnostics = {
42
35
  unknownConfiguredTools: [],
43
36
  unavailableTools: [],
44
- unmatchedTags: [],
45
- missingMcpGatewayTools: [],
46
37
  };
47
38
 
48
39
  const addRegisteredTool = (toolName: string, source: "config" | "policy"): void => {
@@ -69,7 +60,6 @@ export function planActiveTools(input: PlanActiveToolsInput): ActiveToolPlan {
69
60
  for (const toolName of CONTEXT_MODE_TOOL_NAMES) {
70
61
  if (!RARE_CONTEXT_TOOLS.has(toolName)) addRegisteredTool(toolName, "policy");
71
62
  }
72
- addRegisteredTool(MCPC_MANAGER_TOOL_NAME, "policy");
73
63
  }
74
64
 
75
65
  for (const toolName of getTriggeredTools(input.prompt, BALANCED_KEYWORD_TOOLS)) {
@@ -81,20 +71,6 @@ export function planActiveTools(input: PlanActiveToolsInput): ActiveToolPlan {
81
71
  }
82
72
 
83
73
 
84
- const mcpServers = input.mcpServers ?? {};
85
- const pendingTags = input.pendingTags ?? [];
86
- const knownServerNames = new Set(Object.keys(mcpServers));
87
- for (const tag of pendingTags) {
88
- if (!knownServerNames.has(tag)) diagnostics.unmatchedTags.push(tag);
89
- }
90
- for (const serverName of computeActiveServers(mcpServers, input.prompt, pendingTags)) {
91
- const gatewayToolName = `mcpc_${serverName}`;
92
- if (registeredOwnedTools.has(gatewayToolName)) {
93
- selectedOwnedTools.add(gatewayToolName);
94
- } else {
95
- diagnostics.missingMcpGatewayTools.push(gatewayToolName);
96
- }
97
- }
98
74
  for (const toolName of getCommandAllowlistTools(input.prompt, input.lazyTools.commandAllowlist)) {
99
75
  addRegisteredTool(toolName, "config");
100
76
  }
@@ -206,7 +182,5 @@ function dedupeDiagnostics(diagnostics: ActiveToolPlannerDiagnostics): ActiveToo
206
182
  return {
207
183
  unknownConfiguredTools: [...new Set(diagnostics.unknownConfiguredTools)],
208
184
  unavailableTools: [...new Set(diagnostics.unavailableTools)],
209
- unmatchedTags: [...new Set(diagnostics.unmatchedTags)],
210
- missingMcpGatewayTools: [...new Set(diagnostics.missingMcpGatewayTools)],
211
185
  };
212
186
  }
@@ -14,9 +14,6 @@ export const CONTEXT_MODE_TOOL_NAMES = [
14
14
 
15
15
  export type ContextModeToolName = (typeof CONTEXT_MODE_TOOL_NAMES)[number];
16
16
 
17
- export const MCPC_MANAGER_TOOL_NAME = "mcpc_manager";
18
- export const MCPC_TOOL_PREFIX = "mcpc_";
19
-
20
17
  export const OWNED_TOOL_PRIORITY = [
21
18
  "ctx_execute",
22
19
  "ctx_search",
@@ -29,7 +26,6 @@ export const OWNED_TOOL_PRIORITY = [
29
26
  "ctx_purge",
30
27
  "ctx_repomap",
31
28
  "ctx_symbol",
32
- MCPC_MANAGER_TOOL_NAME,
33
29
  ] as const;
34
30
 
35
31
  const CONTEXT_MODE_TOOL_SET = new Set<string>(CONTEXT_MODE_TOOL_NAMES);
@@ -81,12 +77,8 @@ export function isContextModeTool(name: string): boolean {
81
77
  return CONTEXT_MODE_TOOL_SET.has(name);
82
78
  }
83
79
 
84
- export function isMcpcGatewayTool(name: string): boolean {
85
- return name.startsWith(MCPC_TOOL_PREFIX) && /^mcpc_[^_].+/.test(name);
86
- }
87
-
88
80
  export function isSupiOwnedTool(name: string): boolean {
89
- return isContextModeTool(name) || name === MCPC_MANAGER_TOOL_NAME || isMcpcGatewayTool(name);
81
+ return isContextModeTool(name);
90
82
  }
91
83
 
92
84
  export function orderOwnedTools(names: Iterable<string>): string[] {
package/src/types.ts CHANGED
@@ -545,12 +545,6 @@ export interface ContextModeConfig {
545
545
  memory: ContextModeMemoryConfig;
546
546
  }
547
547
 
548
- /** MCP management settings */
549
- export interface McpManagementConfig {
550
- /** Close mcpc sessions on agent shutdown (default: false) */
551
- closeSessionsOnExit: boolean;
552
- }
553
-
554
548
  /** MemPalace native integration default wing derivation mode */
555
549
  export type MempalaceWingStrategy = "repo-name" | "project-slug" | "explicit";
556
550
 
@@ -590,10 +584,19 @@ export interface MempalaceConfig {
590
584
  * one-line refresher instead. `1` = always inject (legacy behavior).
591
585
  */
592
586
  wakeUpInjectionEvery: number;
587
+ /** Minimum cosine similarity (0–1) for a hit to be injected by auto-search. Default 0.55. */
588
+ autoSearchSimilarityFloor: number;
589
+ /** Minimum BM25 score for a hit to be injected by auto-search. Default 0.3. */
590
+ autoSearchBm25Floor: number;
593
591
  };
594
592
  timeouts: {
595
593
  setupMs: number;
596
594
  bridgeMs: number;
595
+ /**
596
+ * Per-hook bridge timeout in milliseconds. Keep this at or above 6000
597
+ * when autoSearchOnPrompt is enabled; MemPalace 3.3.5 can sleep before
598
+ * retrying a transient search-index lookup.
599
+ */
597
600
  hookMs: number;
598
601
  };
599
602
  }
@@ -665,7 +668,6 @@ export interface SupipowersConfig {
665
668
  };
666
669
  ultraplan: UltraPlanConfig;
667
670
  contextMode: ContextModeConfig;
668
- mcp: McpManagementConfig;
669
671
  mempalace: MempalaceConfig;
670
672
  }
671
673
 
@@ -1542,6 +1544,7 @@ export type HarnessStage =
1542
1544
  | "design"
1543
1545
  | "plan"
1544
1546
  | "implement"
1547
+ | "docs"
1545
1548
  | "validate";
1546
1549
 
1547
1550
  /** Operational status of a harness stage. Mirrors UltraPlanAuthoringStageStatus. */
@@ -1682,6 +1685,38 @@ export interface HarnessConfig {
1682
1685
  backend?: HarnessAntiSlopBackend;
1683
1686
  /** Threshold above which Implement defers to ultraplan batch. Default 10. */
1684
1687
  implement_in_session_threshold?: number;
1688
+ /** Per-layer agent-docs stage config. Absent → defaults treated as "simple" tier. */
1689
+ docs?: HarnessDocsConfig;
1690
+ }
1691
+
1692
+ /** Per-layer agent-docs configuration. */
1693
+ export interface HarnessDocsConfig {
1694
+ /**
1695
+ * Doc tier toggle. `simple` makes the `docs` stage a no-op (Tier 1 docs unchanged);
1696
+ * `extensive` fans out one subagent per layer to produce `docs/layers/<id>.md` and a
1697
+ * mechanical `docs/README.md` index.
1698
+ */
1699
+ tier: "simple" | "extensive";
1700
+ /** Hard cap on total LOC per per-layer doc, including frontmatter. Default 150. */
1701
+ max_per_doc_loc: number;
1702
+ /** Hard cap on the `## Agent context` section LOC. Default 30. */
1703
+ agent_context_loc: number;
1704
+ /** Hard cap on `docs/README.md` LOC. Default 50. */
1705
+ max_index_loc: number;
1706
+ /** Defensive cap on the number of layers the stage will process. Default 12. */
1707
+ max_units: number;
1708
+ /**
1709
+ * Concurrency cap for subagent dispatch. `null` = unbounded (bounded only by `max_units`);
1710
+ * any positive integer caps `Promise.all` parallelism.
1711
+ */
1712
+ max_concurrent_subagents: number | null;
1713
+ /** Validate-stage drift warning toggle. */
1714
+ drift_warning: { enabled: boolean };
1715
+ /**
1716
+ * Minimum stale-layer count before bare-entry Harden surfaces the pre-regen preview.
1717
+ * Default 1 (always show when any layer is stale).
1718
+ */
1719
+ regen_preview_threshold: number;
1685
1720
  }
1686
1721
 
1687
1722
  /** Discover artifact (`<session>/discover.json`). */
@@ -1704,7 +1739,6 @@ export interface HarnessDiscoverArtifact {
1704
1739
  hasSupipowers: boolean;
1705
1740
  skills: string[];
1706
1741
  reviewAgents: string[];
1707
- mcpServers: string[];
1708
1742
  plansCount: number;
1709
1743
  };
1710
1744
  /** Existing anti-slop tooling. */
@@ -1762,6 +1796,18 @@ export interface HarnessQualityGate {
1762
1796
  failSafe: string;
1763
1797
  }
1764
1798
 
1799
+ /** Configuration for the PR sticky comment posted by `/supi:harness pr-comment`. */
1800
+ export interface HarnessPrCommentConfig {
1801
+ /** When false, the workflow step is a no-op (still safe to call). */
1802
+ enabled: boolean;
1803
+ /**
1804
+ * Post cadence:
1805
+ * - "every-push": update the sticky comment on every CI run.
1806
+ * - "on-status-change": only re-post when status (passed/warned/failed) flips.
1807
+ */
1808
+ mode: "every-push" | "on-status-change";
1809
+ }
1810
+
1765
1811
  /** CI and local counterpart wiring chosen during harness design. */
1766
1812
  export interface HarnessCiConfig {
1767
1813
  provider: "github-actions";
@@ -1772,6 +1818,54 @@ export interface HarnessCiConfig {
1772
1818
  localCommand: string;
1773
1819
  /** CI workflow path relative to repo root. */
1774
1820
  workflowPath: string;
1821
+ /**
1822
+ * Optional PR comment behaviour. When absent the subcommand falls back to built-in
1823
+ * defaults (mode `every-push`); the explicit invocation of `/supi:harness pr-comment`
1824
+ * is treated as the user opt-in. Set `enabled: false` to suppress posting outside
1825
+ * `--dry-run`. The CI workflow permission warning in `ci-local-wiring` is gated on
1826
+ * an explicit truthy `enabled`, so legacy specs do not trip it.
1827
+ */
1828
+ prComment?: HarnessPrCommentConfig;
1829
+ /**
1830
+ * Optional Git topology + branch-protection wiring captured by the interactive
1831
+ * `git-verify` sub-step run between Design and Plan. Absent on legacy specs.
1832
+ *
1833
+ * - `mainBranch` is the canonical protected branch (typically `main` or `master`).
1834
+ * - `devBranch` is the development branch dev work flows through; `null` when the user
1835
+ * opts out of the convention.
1836
+ * - `enforceMainFromDevOnly` controls both the CI-side guardrail (a `verify-pr-source`
1837
+ * job appended to the rendered workflow) and the opportunistic server-side ruleset
1838
+ * applied via `gh api`. The CI guardrail is deterministic; the ruleset is best-effort.
1839
+ * - `verification` records what the interactive helper actually did. `appliedProtections`
1840
+ * is the set of enforcement layers that landed (e.g. `"ci-guardrail"`, `"ruleset"`).
1841
+ * `findings` carries non-fatal issues surfaced during the run; the validate stage
1842
+ * folds them into its report. `manualInstructionsPath` points at the rendered
1843
+ * fallback doc when `gh` is unavailable or lacks scope.
1844
+ */
1845
+ git?: HarnessCiGitConfig;
1846
+ }
1847
+
1848
+ /** Git/branch-protection block recorded by the interactive verification helper. */
1849
+ export interface HarnessCiGitConfig {
1850
+ mainBranch: string;
1851
+ devBranch: string | null;
1852
+ enforceMainFromDevOnly: boolean;
1853
+ verification: HarnessCiGitVerification | null;
1854
+ }
1855
+
1856
+ /** Result block recorded by `runGitVerificationQa` for downstream stages to consume. */
1857
+ export interface HarnessCiGitVerification {
1858
+ checkedAt: string;
1859
+ appliedProtections: string[];
1860
+ findings: HarnessCiGitFinding[];
1861
+ /** Relative path (under the session dir) to the rendered manual-instructions doc, or null. */
1862
+ manualInstructionsPath: string | null;
1863
+ }
1864
+
1865
+ export interface HarnessCiGitFinding {
1866
+ severity: "info" | "warning" | "error";
1867
+ message: string;
1868
+ remediation?: string;
1775
1869
  }
1776
1870
 
1777
1871
 
@@ -1880,6 +1974,8 @@ export interface HarnessSession {
1880
1974
  iteration: number;
1881
1975
  /** Re-run mode user chose at bare entry (when applicable). */
1882
1976
  reRunMode?: HarnessReRunMode;
1977
+ /** Per-layer agent-docs tier resolved at end-of-Design. Absent → treated as "simple". */
1978
+ docsTier?: "simple" | "extensive";
1883
1979
  /** Recorded blocker, if any. */
1884
1980
  blocker: { code: string; message: string; detectedAt: string } | null;
1885
1981
  /** Artifacts produced so far (relative to <session>/). */
@@ -1895,6 +1991,29 @@ export interface HarnessArtifactRefs {
1895
1991
  plan?: string;
1896
1992
  implementLog?: string;
1897
1993
  validateReport?: string;
1994
+ /** Per-layer agent docs (relative to <session>/docs/layers/<id>.md). */
1995
+ docs?: { layerId: string; path: string }[];
1996
+ }
1997
+
1998
+ /**
1999
+ * Metadata describing a single per-layer agent knowledge document. Used by the docs stage
2000
+ * to track provenance, source-hash invalidation, and atomic promotion to the repo. The
2001
+ * canonical rendered doc lives at `docs/layers/<id>.md`; this record is part of the
2002
+ * stage's run result and the session staging artifacts.
2003
+ */
2004
+ export interface HarnessDocsArtifact {
2005
+ /** Layer id this doc covers (matches HarnessLayerRule.layer). */
2006
+ layerId: string;
2007
+ /** Layer glob list, copied verbatim from the layer rule at render time. */
2008
+ layerGlobs: string[];
2009
+ /** Hash of every input that should invalidate the doc when changed. */
2010
+ sourceHash: string;
2011
+ /** Hash of the doc body after the provenance marker (excludes the marker itself). */
2012
+ contentHash: string;
2013
+ /** ISO timestamp the doc was generated. */
2014
+ generatedAt: string;
2015
+ /** Session that generated the doc. */
2016
+ sessionId: string;
1898
2017
  }
1899
2018
 
1900
2019
  /** Append-only pipeline log entry. */
@@ -108,15 +108,19 @@ function snapshotSessionProgress(sessionDir: string): string | null {
108
108
  for (const entry of entries) {
109
109
  const absolutePath = path.join(currentDir, entry.name);
110
110
  const relativePath = path.relative(sessionDir, absolutePath);
111
- const stats = fs.statSync(absolutePath);
112
-
113
111
  if (entry.isDirectory()) {
114
- hash.update(`dir:${relativePath}:${stats.mtimeMs}\n`);
112
+ hash.update("dir\0");
113
+ hash.update(relativePath);
114
+ hash.update("\0");
115
115
  visit(absolutePath);
116
116
  continue;
117
117
  }
118
118
 
119
- hash.update(`file:${relativePath}:${stats.size}:${stats.mtimeMs}\n`);
119
+ hash.update("file\0");
120
+ hash.update(relativePath);
121
+ hash.update("\0");
122
+ hash.update(fs.readFileSync(absolutePath));
123
+ hash.update("\0");
120
124
  }
121
125
  };
122
126
 
@@ -765,6 +769,106 @@ function discardSessionDir(sessionDir: string): void {
765
769
  }
766
770
  }
767
771
 
772
+ function buildCompletionRepairSteer(
773
+ sessionDir: string,
774
+ manifest: Manifest,
775
+ completionIssues: string[],
776
+ ): string {
777
+ return manifest.backend === "pencil-mcp"
778
+ ? REPAIR_COMPLETE_STEER_TEMPLATE_PENCIL(sessionDir, manifest.penFilePath, completionIssues)
779
+ : REPAIR_COMPLETE_STEER_TEMPLATE(sessionDir, completionIssues);
780
+ }
781
+
782
+ function sendNoUiPauseMessage(
783
+ platform: Platform,
784
+ sessionDir: string,
785
+ status: ManifestStatus,
786
+ ): void {
787
+ const message = [
788
+ `ui-design session paused with status \`${status}\` in a no-UI runtime.`,
789
+ `Artifacts were preserved at \`${sessionDir}\`.`,
790
+ "Interactive review is unavailable here. Continue manually by inspecting that directory or rerun `/supi:ui-design` in an interactive TUI.",
791
+ ].join("\n");
792
+ platform.sendMessage(
793
+ {
794
+ customType: "supi-ui-design-paused-no-ui",
795
+ content: [{ type: "text", text: message }],
796
+ display: true,
797
+ },
798
+ { deliverAs: "steer", triggerTurn: false },
799
+ );
800
+ }
801
+
802
+ async function handleNoUiUiDesignAgentEnd(
803
+ platform: Platform,
804
+ ctx: any,
805
+ session: UiDesignSession,
806
+ manifest: Manifest | null,
807
+ ): Promise<void> {
808
+ const sessionDir = session.dir;
809
+
810
+ if (!manifest) {
811
+ await runCleanup();
812
+ ctx?.ui?.notify?.(
813
+ `ui-design session state is unreadable; artifacts preserved at ${sessionDir}.`,
814
+ "warning",
815
+ );
816
+ cancelUiDesignTracking("manifest_missing_no_ui");
817
+ return;
818
+ }
819
+
820
+ if (manifest.status === "complete") {
821
+ const completion = validateCompletionProof(sessionDir, manifest);
822
+ const validatedManifest = completion.validatedManifest;
823
+ if (
824
+ !sameCritiqueSummary(manifest.critique, validatedManifest.critique) ||
825
+ manifest.approvedAt !== validatedManifest.approvedAt
826
+ ) {
827
+ writeManifest(sessionDir, validatedManifest);
828
+ }
829
+
830
+ if (completion.issues.length > 0) {
831
+ await resumeSession(
832
+ platform,
833
+ ctx,
834
+ session,
835
+ buildCompletionRepairSteer(sessionDir, manifest, completion.issues),
836
+ );
837
+ return;
838
+ }
839
+
840
+ writeManifest(sessionDir, { ...validatedManifest, acknowledged: true });
841
+ await runCleanup();
842
+ ctx?.ui?.notify?.(`ui-design complete; artifacts kept at ${sessionDir}.`, "info");
843
+ cancelUiDesignTracking("complete_no_ui");
844
+ return;
845
+ }
846
+
847
+ if (manifest.status === "discarded") {
848
+ await runCleanup();
849
+ discardSessionDir(sessionDir);
850
+ cancelUiDesignTracking("discarded_no_ui");
851
+ return;
852
+ }
853
+
854
+ if (
855
+ manifest.status === "in-progress" ||
856
+ manifest.status === "critiquing" ||
857
+ manifest.status === "awaiting-review"
858
+ ) {
859
+ if (sessionMadeProgress(sessionDir)) {
860
+ await resumeSession(platform, ctx, session, RESUME_STEER_TEMPLATE(sessionDir));
861
+ return;
862
+ }
863
+
864
+ sendNoUiPauseMessage(platform, sessionDir, manifest.status);
865
+ ctx?.ui?.notify?.(`ui-design paused; artifacts preserved at ${sessionDir}.`, "warning");
866
+ await runCleanup();
867
+ cancelUiDesignTracking("paused_no_ui");
868
+ }
869
+ }
870
+
871
+
768
872
  function getUiDesignWritePaths(toolName: string, input: Record<string, unknown>): string[] | undefined {
769
873
  switch (toolName) {
770
874
  case "write":
@@ -850,11 +954,15 @@ export function registerUiDesignToolGuard(platform: Platform): void {
850
954
  export function registerUiDesignApprovalHook(platform: Platform): void {
851
955
  platform.on("agent_end", async (_event: any, ctx: any) => {
852
956
  const session = activeSession;
853
- if (!session || !ctx?.hasUI) return;
957
+ if (!session) return;
854
958
 
855
959
  const sessionDir = session.dir;
856
960
  const manifest = readManifest(sessionDir);
857
961
 
962
+ if (!ctx?.hasUI) {
963
+ await handleNoUiUiDesignAgentEnd(platform, ctx, session, manifest);
964
+ return;
965
+ }
858
966
  // Missing / unparseable manifest — unsafe to resume
859
967
  if (!manifest) {
860
968
  const choice = await ctx.ui.select(
@@ -888,9 +996,7 @@ export function registerUiDesignApprovalHook(platform: Platform): void {
888
996
  // Pencil manifests always cite pencil artifacts in their repair steer,
889
997
  // even when `penFilePath` is missing — the HTML template would point
890
998
  // at files pencil sessions never produce.
891
- const repairSteer = manifest.backend === "pencil-mcp"
892
- ? REPAIR_COMPLETE_STEER_TEMPLATE_PENCIL(sessionDir, manifest.penFilePath, completion.issues)
893
- : REPAIR_COMPLETE_STEER_TEMPLATE(sessionDir, completion.issues);
999
+ const repairSteer = buildCompletionRepairSteer(sessionDir, manifest, completion.issues);
894
1000
  await resumeSession(
895
1001
  platform,
896
1002
  ctx,
@@ -36,8 +36,17 @@ function isFile(filePath: string): boolean {
36
36
  }
37
37
  }
38
38
 
39
+ function defaultSearchPath(): string {
40
+ if (process.env.PATH !== undefined) {
41
+ return process.env.PATH;
42
+ }
43
+
44
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path");
45
+ return pathKey ? process.env[pathKey] ?? "" : "";
46
+ }
47
+
39
48
  function resolveSearchDirectories(options: ExecutableSearchOptions): string[] {
40
- const pathDirs = (options.searchPath ?? process.env.PATH ?? "")
49
+ const pathDirs = (options.searchPath ?? defaultSearchPath())
41
50
  .split(path.delimiter)
42
51
  .filter(Boolean);
43
52
  const localDirs = (options.localDirs ?? []).map((dir) =>
@@ -17,7 +17,7 @@ function splitWorkspacePath(relativeDir: string): string[] {
17
17
  // Team-shared (local) state: <cwd-or-repoRoot>/.omp/supipowers/<...>
18
18
  //
19
19
  // These paths are committed (or at least shareable) across a team clone. Used for
20
- // config.json, model.json, review-agents/config.yml, mcpc manifests, etc.
20
+ // config.json, model.json, review-agents/config.yml, etc.
21
21
  // ──────────────────────────────────────────────────────────────────────────
22
22
 
23
23
  export function getRootStateDir(paths: PlatformPaths, repoRoot: string): string {