supipowers 2.0.2 → 2.1.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 (76) 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 +5 -133
  5. package/src/config/defaults.ts +5 -5
  6. package/src/config/loader.ts +1 -0
  7. package/src/config/schema.ts +2 -6
  8. package/src/context-mode/knowledge/store.ts +381 -43
  9. package/src/context-mode/tools.ts +41 -3
  10. package/src/deps/registry.ts +1 -12
  11. package/src/fix-pr/assessment.ts +1 -0
  12. package/src/fix-pr/prompt-builder.ts +1 -0
  13. package/src/git/commit.ts +76 -18
  14. package/src/harness/command.ts +103 -6
  15. package/src/harness/default-agents/docs.md +39 -0
  16. package/src/harness/docs/config.ts +29 -0
  17. package/src/harness/docs/glob-match.ts +27 -0
  18. package/src/harness/docs/index-renderer.ts +82 -0
  19. package/src/harness/docs/provenance.ts +125 -0
  20. package/src/harness/docs/regen-decision.ts +167 -0
  21. package/src/harness/docs/representative-files.ts +175 -0
  22. package/src/harness/docs/source-hash.ts +106 -0
  23. package/src/harness/docs/validator.ts +233 -0
  24. package/src/harness/hooks/layer-context-inject.ts +35 -1
  25. package/src/harness/hooks/register.ts +24 -3
  26. package/src/harness/pipeline.ts +20 -5
  27. package/src/harness/pr-comment/baseline.ts +105 -0
  28. package/src/harness/pr-comment/ci-env.ts +120 -0
  29. package/src/harness/pr-comment/gh-poster.ts +227 -0
  30. package/src/harness/pr-comment/handler.ts +198 -0
  31. package/src/harness/pr-comment/render.ts +297 -0
  32. package/src/harness/pr-comment/status.ts +95 -0
  33. package/src/harness/pr-comment/types.ts +73 -0
  34. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  35. package/src/harness/project-paths.ts +95 -0
  36. package/src/harness/stages/design.ts +1 -0
  37. package/src/harness/stages/discover.ts +1 -13
  38. package/src/harness/stages/docs.ts +708 -0
  39. package/src/harness/stages/implement-apply.ts +877 -0
  40. package/src/harness/stages/implement.ts +64 -51
  41. package/src/harness/stages/plan.ts +25 -16
  42. package/src/harness/stages/validate.ts +370 -0
  43. package/src/harness/storage.ts +142 -0
  44. package/src/harness/tools.ts +130 -0
  45. package/src/mempalace/bridge.ts +207 -41
  46. package/src/mempalace/config.ts +10 -4
  47. package/src/mempalace/format.ts +122 -6
  48. package/src/mempalace/hooks.ts +204 -56
  49. package/src/mempalace/installer-helper.ts +18 -4
  50. package/src/mempalace/python/mempalace_bridge.py +128 -3
  51. package/src/mempalace/runtime.ts +53 -16
  52. package/src/mempalace/schema.ts +151 -30
  53. package/src/mempalace/session-summary.ts +5 -0
  54. package/src/mempalace/tool.ts +17 -4
  55. package/src/mempalace/upstream-limits.ts +69 -0
  56. package/src/planning/approval-flow.ts +25 -2
  57. package/src/planning/planning-ask-tool.ts +34 -4
  58. package/src/planning/system-prompt.ts +1 -1
  59. package/src/tool-catalog/active-tool-controller.ts +0 -22
  60. package/src/tool-catalog/active-tool-planner.ts +0 -26
  61. package/src/tool-catalog/tool-groups.ts +1 -9
  62. package/src/types.ts +87 -8
  63. package/src/ui-design/session.ts +114 -8
  64. package/src/utils/executable.ts +10 -1
  65. package/src/workspace/state-paths.ts +1 -1
  66. package/src/commands/mcp.ts +0 -814
  67. package/src/mcp/activation.ts +0 -77
  68. package/src/mcp/config.ts +0 -223
  69. package/src/mcp/docs.ts +0 -154
  70. package/src/mcp/gateway.ts +0 -103
  71. package/src/mcp/lifecycle.ts +0 -79
  72. package/src/mcp/manager-tool.ts +0 -104
  73. package/src/mcp/mcpc.ts +0 -113
  74. package/src/mcp/registry.ts +0 -98
  75. package/src/mcp/triggers.ts +0 -62
  76. package/src/mcp/types.ts +0 -95
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Upstream MemPalace pin.
3
+ *
4
+ * Single source of truth for the `mempalace` PyPI package version and
5
+ * the parameter limits the upstream package enforces. Keeping these in
6
+ * one place — instead of sprinkling `"3.3.4"` / `500` / `100` / `128`
7
+ * magic literals across the config defaults, schema, hooks, and tests —
8
+ * makes a version bump a one-line edit and guarantees our tool surface
9
+ * advertises the same bounds the upstream MCP server enforces.
10
+ *
11
+ * # Bump procedure
12
+ * 1. Update `MEMPALACE_PACKAGE_VERSION` below.
13
+ * 2. Re-verify each `MEMPALACE_MAX_*` constant against the cited
14
+ * upstream source path. Update any that drifted.
15
+ * 3. If the upstream MCP API surface (function names, parameter names)
16
+ * changed, update the dispatch table in
17
+ * `src/mempalace/python/mempalace_bridge.py` and its header comment.
18
+ * 4. Run `bun ci`. All consumers — including tests — read from these
19
+ * constants, so a mismatch surfaces as a test failure rather than
20
+ * silent runtime drift.
21
+ */
22
+
23
+ /**
24
+ * Exact PyPI version installed by the managed setup pipeline. Flows into
25
+ * `DEFAULT_CONFIG.mempalace.packageVersion` and, from there, into the
26
+ * `mempalace==<version>` argument handed to `uv pip install`.
27
+ */
28
+ export const MEMPALACE_PACKAGE_VERSION = "3.3.5";
29
+
30
+ /**
31
+ * Upper bound applied internally by `tool_search` and `tool_list_drawers`
32
+ * to the `limit` argument. Any value above this is silently clamped.
33
+ *
34
+ * Source: `mempalace/mcp_server.py` `_MAX_RESULTS = 100`.
35
+ */
36
+ export const MEMPALACE_MAX_RESULTS = 100;
37
+
38
+ /**
39
+ * Maximum search-query length. `tool_search` runs `sanitize_query`, which
40
+ * truncates anything over this threshold (worst case: keeps only the
41
+ * trailing N characters). Above this, prompt-contamination patterns
42
+ * start dominating the embedding signal — see upstream Issue #333.
43
+ *
44
+ * Source: `mempalace/query_sanitizer.py` `MAX_QUERY_LENGTH = 250`.
45
+ */
46
+ export const MEMPALACE_MAX_QUERY_LENGTH = 250;
47
+
48
+ /**
49
+ * Maximum length for wing / room / predicate / entity-style identifiers.
50
+ * `sanitize_name` and `sanitize_kg_value` raise `ValueError` above this.
51
+ *
52
+ * Source: `mempalace/config.py` `MAX_NAME_LENGTH = 128`.
53
+ */
54
+ export const MEMPALACE_MAX_NAME_LENGTH = 128;
55
+
56
+ /**
57
+ * Maximum drawer / diary content length. `sanitize_content` defaults
58
+ * to this when no explicit override is passed.
59
+ *
60
+ * Source: `mempalace/config.py` `sanitize_content(..., max_length: int = 100_000)`.
61
+ */
62
+ export const MEMPALACE_MAX_CONTENT_LENGTH = 100_000;
63
+
64
+ /**
65
+ * Upper bound applied internally by `tool_traverse_graph` to `max_hops`.
66
+ *
67
+ * Source: `mempalace/mcp_server.py` `tool_traverse_graph` — `max(1, min(max_hops, 10))`.
68
+ */
69
+ export const MEMPALACE_MAX_HOPS = 10;
@@ -186,7 +186,7 @@ export function buildTodoWriteOpsForPlan(plan: Plan): { ops: TodoWriteOp[] } {
186
186
  * When `plan` is provided and has tasks, the prompt also embeds the
187
187
  * exact `todo_write` payload the agent must call before doing any work.
188
188
  */
189
- function buildExecutionPrompt(
189
+ export function buildExecutionPrompt(
190
190
  planContent: string,
191
191
  planPath: string,
192
192
  plan?: Plan,
@@ -313,7 +313,7 @@ async function executeApproveFlow(
313
313
  */
314
314
  export function registerPlanApprovalHook(platform: Platform): void {
315
315
  platform.on("agent_end", async (_event: any, ctx: any) => {
316
- if (!planningActive || !ctx?.hasUI || approvalPending) return;
316
+ if (!planningActive || approvalPending) return;
317
317
 
318
318
  // Detect newly written plan files
319
319
  const plansNow = listPlans(platform.paths, planCwd);
@@ -407,6 +407,29 @@ export function registerPlanApprovalHook(platform: Platform): void {
407
407
  cwd: planCwd,
408
408
  });
409
409
  } catch {}
410
+ if (!ctx?.hasUI) {
411
+ const message = [
412
+ `Plan saved to \`${planPath}\`.`,
413
+ "Interactive approval is unavailable in this runtime, so no execution was started.",
414
+ `To continue manually, explicitly send: \`Execute the saved plan at ${planPath} step by step; verify each step before proceeding.\``,
415
+ ].join("\n");
416
+ debugLogger?.log("approval_flow_no_ui", {
417
+ planName,
418
+ planPath,
419
+ });
420
+ ctx?.ui?.notify?.("Plan saved; interactive approval is required before execution.", "warning");
421
+ platform.sendMessage(
422
+ {
423
+ customType: "supi-plan-awaiting-interactive-approval",
424
+ content: [{ type: "text", text: message }],
425
+ display: true,
426
+ },
427
+ { deliverAs: "steer", triggerTurn: false },
428
+ );
429
+ cancelPlanTracking();
430
+ return;
431
+ }
432
+
410
433
  const approvalOptions = [
411
434
  "Approve and execute",
412
435
  "Refine plan",
@@ -4,10 +4,16 @@ import { isUiDesignActive, recordUiDesignReviewApproval } from "../ui-design/ses
4
4
 
5
5
  /**
6
6
  * Register a `planning_ask` tool — identical to the built-in `ask` tool
7
- * but with **no timeout**. OMP's built-in ask tool applies the user's
8
- * `ask.timeout` setting (default 30s) and only disables it for OMP's
9
- * native plan mode. Since `/supi:plan` is not native plan mode, planning
10
- * questions would auto-dismiss. This tool bypasses that limitation.
7
+ * but with **no timeout**, regardless of the user's `ask.timeout` setting.
8
+ * OMP 14.9.5 changed the `ask.timeout` default from 30s to 0 (wait
9
+ * indefinitely), but a user-configured non-zero value still applies to the
10
+ * generic `ask` tool; this wrapper keeps planning-mode questions blocking
11
+ * for any such configuration.
12
+ *
13
+ * Also records the chosen option into the ui-design session ledger via
14
+ * `recordUiDesignReviewApproval` and pairs with
15
+ * `registerPlanningAskToolGuard`, which redirects generic `ask` calls back
16
+ * to this tool during planning / ui-design sessions.
11
17
  *
12
18
  * The tool is always registered (lightweight) but the planning system
13
19
  * prompt directs the model to use it only during planning sessions.
@@ -62,6 +68,22 @@ export function registerPlanningAskTool(platform: Platform): void {
62
68
  };
63
69
  }
64
70
 
71
+ if (ctx?.hasUI === false || typeof ctx?.ui?.select !== "function") {
72
+ const result = {
73
+ error: "interactive_planning_question_unavailable",
74
+ message: "Interactive planning questions cannot be answered in this runtime. Present this question and its options to the user instead of choosing a default.",
75
+ question: params.question,
76
+ options: labels,
77
+ recommended: params.recommended ?? null,
78
+ };
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify(result) }],
81
+ details: result,
82
+ error: true,
83
+ };
84
+ }
85
+
86
+
65
87
  const choice = await ctx.ui.select(params.question, labels, {
66
88
  initialIndex: params.recommended,
67
89
  // No timeout — planning decisions need unlimited time
@@ -107,6 +129,14 @@ function getAskRedirectReason(): string | null {
107
129
  */
108
130
  export function registerPlanningAskToolGuard(platform: Platform): void {
109
131
  platform.on("tool_call", (event) => {
132
+ if (event.toolName === "exit_plan_mode" && isPlanningActive()) {
133
+ return {
134
+ block: true,
135
+ reason:
136
+ "Planning mode: /supi:plan uses a file-based approval hook. Do not call exit_plan_mode because it is OMP's native approval path and bypasses supipowers plan tracking.",
137
+ };
138
+ }
139
+
110
140
  if (event.toolName !== "ask") return;
111
141
 
112
142
  const reason = getAskRedirectReason();
@@ -233,7 +233,7 @@ function buildPlanningCriticalBlock(options: PlanningSystemPromptOptions): strin
233
233
  "## Plan submission",
234
234
  "",
235
235
  "This is NOT native OMP plan mode.",
236
- "You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — it will fail.",
236
+ "You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — that is OMP's native approval path and bypasses supipowers' file-based approval hook.",
237
237
  `You **MUST NOT** write plans to \`local://PLAN.md\` — that is OMP's native plan location and will not trigger the approval flow.`,
238
238
  `You **MUST** save the plan to \`${options.plansDir}/YYYY-MM-DD-<feature-name>.md\` using the Write tool.`,
239
239
  "After saving, tell the user the plan path, then **stop and yield your turn**.",
@@ -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,14 @@ 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;
1775
1829
  }
1776
1830
 
1777
1831
 
@@ -1880,6 +1934,8 @@ export interface HarnessSession {
1880
1934
  iteration: number;
1881
1935
  /** Re-run mode user chose at bare entry (when applicable). */
1882
1936
  reRunMode?: HarnessReRunMode;
1937
+ /** Per-layer agent-docs tier resolved at end-of-Design. Absent → treated as "simple". */
1938
+ docsTier?: "simple" | "extensive";
1883
1939
  /** Recorded blocker, if any. */
1884
1940
  blocker: { code: string; message: string; detectedAt: string } | null;
1885
1941
  /** Artifacts produced so far (relative to <session>/). */
@@ -1895,6 +1951,29 @@ export interface HarnessArtifactRefs {
1895
1951
  plan?: string;
1896
1952
  implementLog?: string;
1897
1953
  validateReport?: string;
1954
+ /** Per-layer agent docs (relative to <session>/docs/layers/<id>.md). */
1955
+ docs?: { layerId: string; path: string }[];
1956
+ }
1957
+
1958
+ /**
1959
+ * Metadata describing a single per-layer agent knowledge document. Used by the docs stage
1960
+ * to track provenance, source-hash invalidation, and atomic promotion to the repo. The
1961
+ * canonical rendered doc lives at `docs/layers/<id>.md`; this record is part of the
1962
+ * stage's run result and the session staging artifacts.
1963
+ */
1964
+ export interface HarnessDocsArtifact {
1965
+ /** Layer id this doc covers (matches HarnessLayerRule.layer). */
1966
+ layerId: string;
1967
+ /** Layer glob list, copied verbatim from the layer rule at render time. */
1968
+ layerGlobs: string[];
1969
+ /** Hash of every input that should invalidate the doc when changed. */
1970
+ sourceHash: string;
1971
+ /** Hash of the doc body after the provenance marker (excludes the marker itself). */
1972
+ contentHash: string;
1973
+ /** ISO timestamp the doc was generated. */
1974
+ generatedAt: string;
1975
+ /** Session that generated the doc. */
1976
+ sessionId: string;
1898
1977
  }
1899
1978
 
1900
1979
  /** 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,