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.
- package/README.md +5 -6
- package/package.json +4 -2
- package/skills/harness/SKILL.md +1 -0
- package/src/bootstrap.ts +8 -133
- package/src/commands/optimize-context.ts +153 -16
- package/src/commands/runbook.ts +511 -0
- package/src/config/defaults.ts +5 -5
- package/src/config/loader.ts +1 -0
- package/src/config/schema.ts +2 -6
- package/src/context/rule-renderer.ts +274 -2
- package/src/context/runbook-extension-template.ts +193 -0
- package/src/context/startup-check.ts +197 -2
- package/src/context/startup-optimizer.ts +133 -10
- package/src/context-mode/knowledge/store.ts +381 -43
- package/src/context-mode/tools.ts +41 -3
- package/src/deps/registry.ts +1 -12
- package/src/fix-pr/assessment.ts +1 -0
- package/src/fix-pr/prompt-builder.ts +1 -0
- package/src/git/commit.ts +76 -18
- package/src/harness/command.ts +201 -12
- package/src/harness/default-agents/docs.md +39 -0
- package/src/harness/docs/config.ts +29 -0
- package/src/harness/docs/glob-match.ts +27 -0
- package/src/harness/docs/index-renderer.ts +82 -0
- package/src/harness/docs/provenance.ts +125 -0
- package/src/harness/docs/regen-decision.ts +167 -0
- package/src/harness/docs/representative-files.ts +175 -0
- package/src/harness/docs/source-hash.ts +106 -0
- package/src/harness/docs/validator.ts +233 -0
- package/src/harness/git-verification.ts +515 -0
- package/src/harness/git-verify-qa.ts +406 -0
- package/src/harness/hooks/layer-context-inject.ts +35 -1
- package/src/harness/hooks/register.ts +24 -3
- package/src/harness/pipeline.ts +37 -13
- package/src/harness/pr-comment/baseline.ts +105 -0
- package/src/harness/pr-comment/ci-env.ts +120 -0
- package/src/harness/pr-comment/gh-poster.ts +227 -0
- package/src/harness/pr-comment/handler.ts +198 -0
- package/src/harness/pr-comment/render.ts +297 -0
- package/src/harness/pr-comment/status.ts +95 -0
- package/src/harness/pr-comment/types.ts +73 -0
- package/src/harness/pr-comment/workflow-summary.ts +47 -0
- package/src/harness/project-paths.ts +95 -0
- package/src/harness/stages/design.ts +1 -0
- package/src/harness/stages/discover.ts +1 -13
- package/src/harness/stages/docs.ts +708 -0
- package/src/harness/stages/implement-apply.ts +934 -0
- package/src/harness/stages/implement.ts +64 -51
- package/src/harness/stages/plan.ts +25 -16
- package/src/harness/stages/validate.ts +478 -0
- package/src/harness/storage.ts +142 -0
- package/src/harness/tools.ts +130 -0
- package/src/mempalace/bridge.ts +207 -41
- package/src/mempalace/config.ts +10 -4
- package/src/mempalace/format.ts +122 -6
- package/src/mempalace/hooks.ts +204 -56
- package/src/mempalace/installer-helper.ts +18 -4
- package/src/mempalace/python/mempalace_bridge.py +128 -3
- package/src/mempalace/runtime.ts +53 -16
- package/src/mempalace/schema.ts +151 -30
- package/src/mempalace/session-summary.ts +5 -0
- package/src/mempalace/tool.ts +17 -4
- package/src/mempalace/upstream-limits.ts +69 -0
- package/src/planning/approval-flow.ts +25 -2
- package/src/planning/planning-ask-tool.ts +34 -4
- package/src/planning/system-prompt.ts +1 -1
- package/src/tool-catalog/active-tool-controller.ts +0 -22
- package/src/tool-catalog/active-tool-planner.ts +0 -26
- package/src/tool-catalog/tool-groups.ts +1 -9
- package/src/types.ts +127 -8
- package/src/ui-design/session.ts +114 -8
- package/src/utils/executable.ts +10 -1
- package/src/workspace/state-paths.ts +1 -1
- package/src/commands/mcp.ts +0 -814
- package/src/mcp/activation.ts +0 -77
- package/src/mcp/config.ts +0 -223
- package/src/mcp/docs.ts +0 -154
- package/src/mcp/gateway.ts +0 -103
- package/src/mcp/lifecycle.ts +0 -79
- package/src/mcp/manager-tool.ts +0 -104
- package/src/mcp/mcpc.ts +0 -113
- package/src/mcp/registry.ts +0 -98
- package/src/mcp/triggers.ts +0 -62
- package/src/mcp/types.ts +0 -95
package/src/git/commit.ts
CHANGED
|
@@ -139,28 +139,27 @@ interface CommitStagingContext {
|
|
|
139
139
|
stagedFiles: string[];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const STAGE_ALL_CHANGES_OPTION = "Stage all changes — include staged, unstaged, and untracked files";
|
|
143
|
+
const USE_STAGED_CHANGES_OPTION = "Use staged changes only — commit the index as-is";
|
|
144
|
+
|
|
145
|
+
async function stageAllChanges(
|
|
146
|
+
exec: ExecFn,
|
|
144
147
|
ctx: any,
|
|
145
148
|
cwd: string,
|
|
146
|
-
|
|
149
|
+
fileCount: number,
|
|
147
150
|
progress: ReturnType<typeof createProgress>,
|
|
148
|
-
): Promise<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (addResult.code !== 0) {
|
|
155
|
-
notifyError(ctx, "git add failed", addResult.stderr || "Non-zero exit");
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
progress.complete(1, `${status.files.length} file(s)`);
|
|
159
|
-
} else {
|
|
160
|
-
progress.activate(1, `${status.stagedFiles.length} staged`);
|
|
161
|
-
progress.complete(1, `${status.stagedFiles.length} staged`);
|
|
151
|
+
): Promise<boolean> {
|
|
152
|
+
progress.activate(1, `${fileCount} file(s)`);
|
|
153
|
+
const addResult = await exec("git", ["add", "-A"], { cwd });
|
|
154
|
+
if (addResult.code !== 0) {
|
|
155
|
+
notifyError(ctx, "git add failed", addResult.stderr || "Non-zero exit");
|
|
156
|
+
return false;
|
|
162
157
|
}
|
|
158
|
+
progress.complete(1, `${fileCount} file(s)`);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
163
161
|
|
|
162
|
+
async function readStagedFiles(exec: ExecFn, ctx: any, cwd: string): Promise<string[] | null> {
|
|
164
163
|
const stagedFilesResult = await exec("git", ["diff", "--cached", "--name-only"], { cwd });
|
|
165
164
|
if (stagedFilesResult.code !== 0) {
|
|
166
165
|
notifyError(ctx, "git diff failed", stagedFilesResult.stderr || "Could not read staged files");
|
|
@@ -173,7 +172,66 @@ async function ensureStagedChanges(
|
|
|
173
172
|
return null;
|
|
174
173
|
}
|
|
175
174
|
|
|
176
|
-
return
|
|
175
|
+
return stagedFiles;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatFilePreview(files: string[], label: string): string {
|
|
179
|
+
const preview = files.slice(0, 8).join("\n");
|
|
180
|
+
const extra = files.length > 8 ? `\n… and ${files.length - 8} more ${label}` : "";
|
|
181
|
+
return `${preview}${extra}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function ensureStagedChanges(
|
|
185
|
+
platform: Platform,
|
|
186
|
+
ctx: any,
|
|
187
|
+
cwd: string,
|
|
188
|
+
status: Awaited<ReturnType<typeof getWorkingTreeStatus>>,
|
|
189
|
+
progress: ReturnType<typeof createProgress>,
|
|
190
|
+
): Promise<CommitStagingContext | null> {
|
|
191
|
+
const exec = platform.exec.bind(platform);
|
|
192
|
+
|
|
193
|
+
if (status.stagedFiles.length > 0 && status.unstagedFiles.length > 0) {
|
|
194
|
+
const selection = await ctx.ui.select(
|
|
195
|
+
"Staged and unstaged changes detected",
|
|
196
|
+
[STAGE_ALL_CHANGES_OPTION, USE_STAGED_CHANGES_OPTION],
|
|
197
|
+
{
|
|
198
|
+
helpText: [
|
|
199
|
+
"Choose the source of truth for /supi:commit.",
|
|
200
|
+
`Staged (${status.stagedFiles.length}):\n${formatFilePreview(status.stagedFiles, "staged")}`,
|
|
201
|
+
`Unstaged/untracked (${status.unstagedFiles.length}):\n${formatFilePreview(status.unstagedFiles, "unstaged")}`,
|
|
202
|
+
].join("\n\n"),
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!selection) {
|
|
207
|
+
progress.dispose();
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (selection === STAGE_ALL_CHANGES_OPTION) {
|
|
212
|
+
if (!await stageAllChanges(exec, ctx, cwd, status.files.length, progress)) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
progress.activate(1, `${status.stagedFiles.length} staged`);
|
|
217
|
+
progress.complete(1, `${status.stagedFiles.length} staged`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const stagedFiles = await readStagedFiles(exec, ctx, cwd);
|
|
221
|
+
return stagedFiles ? { stagedFiles } : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (status.stagedFiles.length === 0) {
|
|
225
|
+
if (!await stageAllChanges(exec, ctx, cwd, status.files.length, progress)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
progress.activate(1, `${status.stagedFiles.length} staged`);
|
|
230
|
+
progress.complete(1, `${status.stagedFiles.length} staged`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const stagedFiles = await readStagedFiles(exec, ctx, cwd);
|
|
234
|
+
return stagedFiles ? { stagedFiles } : null;
|
|
177
235
|
}
|
|
178
236
|
|
|
179
237
|
// ── Main entry point ───────────────────────────────────────
|
package/src/harness/command.ts
CHANGED
|
@@ -39,9 +39,9 @@ import {
|
|
|
39
39
|
loadHarnessSession,
|
|
40
40
|
loadHarnessValidateReport,
|
|
41
41
|
readSlopQueue,
|
|
42
|
+
saveHarnessDesignSpecJson,
|
|
42
43
|
saveHarnessSession,
|
|
43
44
|
} from "./storage.js";
|
|
44
|
-
import { getHarnessSessionDir } from "./project-paths.js";
|
|
45
45
|
import { computeScore } from "./anti_slop/score.js";
|
|
46
46
|
import {
|
|
47
47
|
type BuildRunnerInput,
|
|
@@ -55,6 +55,9 @@ import { newHarnessSessionId } from "./stage-runner.js";
|
|
|
55
55
|
import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
|
|
56
56
|
import { getWorkingTreeStatus } from "../git/status.js";
|
|
57
57
|
import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
58
|
+
import { handlePrComment } from "./pr-comment/handler.js";
|
|
59
|
+
import { runGitVerificationQa } from "./git-verify-qa.js";
|
|
60
|
+
import { getHarnessSessionDir } from "./project-paths.js";
|
|
58
61
|
import type { HarnessDesignSpec, HarnessGateMode, HarnessSession, HarnessStage } from "../types.js";
|
|
59
62
|
|
|
60
63
|
modelRegistry.register({
|
|
@@ -67,6 +70,7 @@ modelRegistry.register({
|
|
|
67
70
|
export interface HarnessCommandContext {
|
|
68
71
|
cwd: string;
|
|
69
72
|
hasUI?: boolean;
|
|
73
|
+
newSession?: (options?: any) => Promise<{ cancelled: boolean }>;
|
|
70
74
|
ui: {
|
|
71
75
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
72
76
|
select?: (title: string, options: unknown[]) => Promise<string | null>;
|
|
@@ -80,6 +84,7 @@ export const HARNESS_SUBCOMMANDS = [
|
|
|
80
84
|
{ name: "design", description: "Run/advance the design stage (requires Discover + Research)" },
|
|
81
85
|
{ name: "plan-draft", description: "Render and persist the plan from the in-flight design spec" },
|
|
82
86
|
{ name: "implement", description: "Route plan to in-session steer or batch" },
|
|
87
|
+
{ name: "docs", description: "Generate per-layer agent docs (extensive mode only)" },
|
|
83
88
|
{ name: "validate", description: "Run validate sub-checks" },
|
|
84
89
|
{ name: "resume", description: "Pick up an in-flight session" },
|
|
85
90
|
{ name: "status", description: "Print stage + score badge" },
|
|
@@ -88,6 +93,7 @@ export const HARNESS_SUBCOMMANDS = [
|
|
|
88
93
|
{ name: "resolve", description: "Mark a queue entry resolved" },
|
|
89
94
|
{ name: "backlog", description: "List every open queue entry" },
|
|
90
95
|
{ name: "score", description: "Recompute and display the score" },
|
|
96
|
+
{ name: "pr-comment", description: "Render or post the harness PR sticky comment" },
|
|
91
97
|
] as const;
|
|
92
98
|
|
|
93
99
|
type HarnessSubcommand = (typeof HARNESS_SUBCOMMANDS)[number]["name"];
|
|
@@ -100,6 +106,7 @@ const HARNESS_STAGE_LABELS: Readonly<Record<HarnessStage, string>> = {
|
|
|
100
106
|
design: "Design harness",
|
|
101
107
|
plan: "Draft plan",
|
|
102
108
|
implement: "Apply artifacts",
|
|
109
|
+
docs: "Generate per-layer docs",
|
|
103
110
|
validate: "Validate results",
|
|
104
111
|
};
|
|
105
112
|
|
|
@@ -122,7 +129,7 @@ export interface HarnessCommandRequest {
|
|
|
122
129
|
// ── Progress (status-bar + one final notification) ───────────────
|
|
123
130
|
|
|
124
131
|
function createHarnessProgress(ctx: HarnessCommandContext) {
|
|
125
|
-
const SO = ["discover", "research", "design", "plan", "implement", "validate"] as HarnessStage[];
|
|
132
|
+
const SO = ["discover", "research", "design", "plan", "implement", "docs", "validate"] as HarnessStage[];
|
|
126
133
|
let done = 0;
|
|
127
134
|
let cur: HarnessStage | null = null;
|
|
128
135
|
const completed: string[] = [];
|
|
@@ -197,8 +204,10 @@ export async function handleHarness(
|
|
|
197
204
|
case "design": await handleStageCommand(platform, ctx, "design", request.args); return;
|
|
198
205
|
case "plan-draft": await handleStageCommand(platform, ctx, "plan", request.args); return;
|
|
199
206
|
case "implement": await handleStageCommand(platform, ctx, "implement", request.args); return;
|
|
207
|
+
case "docs": await handleStageCommand(platform, ctx, "docs", request.args); return;
|
|
200
208
|
case "validate": await handleStageCommand(platform, ctx, "validate", request.args); return;
|
|
201
209
|
case "resume": await handleResume(platform, ctx, request.args); return;
|
|
210
|
+
case "pr-comment": await handlePrComment(platform, ctx, request.args); return;
|
|
202
211
|
default:
|
|
203
212
|
notifyError(ctx, "Unknown harness subcommand", `\`${request.subcommand}\` is not recognized.`);
|
|
204
213
|
return;
|
|
@@ -217,12 +226,13 @@ async function runPipelineWithProgress(
|
|
|
217
226
|
gates: HarnessGateMode,
|
|
218
227
|
stageInputs: BuildRunnerInput,
|
|
219
228
|
startStage?: HarnessStage,
|
|
229
|
+
forceStages?: ReadonlySet<HarnessStage>,
|
|
220
230
|
): Promise<PipelineRunOutcome> {
|
|
221
231
|
const harnessProgress = createHarnessProgress(ctx);
|
|
222
232
|
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
223
233
|
const outcome = await pipelineDriver({
|
|
224
234
|
platform, paths: platform.paths, cwd: ctx.cwd, sessionId,
|
|
225
|
-
modelConfig, gates, stageInputs, startStage,
|
|
235
|
+
modelConfig, gates, stageInputs, startStage, forceStages,
|
|
226
236
|
onProgress: harnessProgress.onProgress,
|
|
227
237
|
});
|
|
228
238
|
// Single consolidated notification.
|
|
@@ -276,7 +286,8 @@ async function presentGateForStage(
|
|
|
276
286
|
`Design spec ready\n\n${summary}\n\nContinue to plan?`,
|
|
277
287
|
["Continue", "Stop — I'll customize the design"],
|
|
278
288
|
);
|
|
279
|
-
|
|
289
|
+
if (choice !== "Continue") return "stop";
|
|
290
|
+
return await promptDocsTierIfNeeded(platform, ctx, sessionId, layers);
|
|
280
291
|
}
|
|
281
292
|
case "plan": {
|
|
282
293
|
const plansDir = getProjectStatePath(platform.paths, ctx.cwd, "plans");
|
|
@@ -293,6 +304,27 @@ async function presentGateForStage(
|
|
|
293
304
|
);
|
|
294
305
|
return choice === "Approve and continue" ? "continue" : "stop";
|
|
295
306
|
}
|
|
307
|
+
case "docs": {
|
|
308
|
+
const session = loadHarnessSession(platform.paths, ctx.cwd, sessionId);
|
|
309
|
+
const tier = session.ok ? (session.value.docsTier ?? "simple") : "simple";
|
|
310
|
+
const docsDir = path.join(ctx.cwd, "docs", "layers");
|
|
311
|
+
let layerCount = 0;
|
|
312
|
+
if (fs.existsSync(docsDir)) {
|
|
313
|
+
try {
|
|
314
|
+
layerCount = fs.readdirSync(docsDir).filter((f) => f.endsWith(".md")).length;
|
|
315
|
+
} catch {
|
|
316
|
+
/* best-effort */
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const summary = tier === "extensive"
|
|
320
|
+
? `Tier: extensive\nPer-layer docs at docs/layers/ (${layerCount} layer file${layerCount === 1 ? "" : "s"}).\nIndex: docs/README.md.`
|
|
321
|
+
: `Tier: simple\nNo per-layer docs were generated.`;
|
|
322
|
+
const choice = await ctx.ui.select(
|
|
323
|
+
`Per-layer agent docs\n\n${summary}\n\nContinue to validate?`,
|
|
324
|
+
["Continue", "Stop — I want to inspect the docs first"],
|
|
325
|
+
);
|
|
326
|
+
return choice === "Continue" ? "continue" : "stop";
|
|
327
|
+
}
|
|
296
328
|
case "validate": {
|
|
297
329
|
const report = loadHarnessValidateReport(platform.paths, ctx.cwd, sessionId);
|
|
298
330
|
const passed = report.ok ? report.value.passed : false;
|
|
@@ -312,6 +344,55 @@ async function presentGateForStage(
|
|
|
312
344
|
}
|
|
313
345
|
}
|
|
314
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Ask the user whether the upcoming docs stage should generate per-layer agent docs.
|
|
349
|
+
*
|
|
350
|
+
* Behavior:
|
|
351
|
+
* - If the session manifest already records a `docsTier`, skip the prompt.
|
|
352
|
+
* - In `auto` gate mode (no UI), default to "simple" silently.
|
|
353
|
+
* - In default/manual modes, prompt the user; "cancel" aborts via `stop` propagated
|
|
354
|
+
* by the caller; "simple"/"extensive" persist on the manifest.
|
|
355
|
+
*
|
|
356
|
+
* Layer count <2 always resolves to "simple" — extensive mode is meaningless with a
|
|
357
|
+
* single-bucket architecture.
|
|
358
|
+
*/
|
|
359
|
+
async function promptDocsTierIfNeeded(
|
|
360
|
+
platform: Platform,
|
|
361
|
+
ctx: HarnessCommandContext,
|
|
362
|
+
sessionId: string,
|
|
363
|
+
layerCount: number,
|
|
364
|
+
): Promise<"continue" | "stop"> {
|
|
365
|
+
const session = loadHarnessSession(platform.paths, ctx.cwd, sessionId);
|
|
366
|
+
if (!session.ok) return "continue";
|
|
367
|
+
const isRerun =
|
|
368
|
+
session.value.reRunMode === "rebuild" || session.value.reRunMode === "harden";
|
|
369
|
+
if (session.value.docsTier && !isRerun) return "continue";
|
|
370
|
+
|
|
371
|
+
let tier: "simple" | "extensive" = session.value.docsTier ?? "simple";
|
|
372
|
+
if (layerCount >= 2 && ctx.ui.select) {
|
|
373
|
+
const currentLabel = session.value.docsTier ? ` (current: ${session.value.docsTier})` : "";
|
|
374
|
+
const summary = `simple — Tier 1 docs only (AGENTS.md, architecture.md, golden-principles.md)\nextensive — Tier 1 + per-layer docs at docs/layers/<id>.md + index at docs/README.md\n (≤150 LOC/doc, ${layerCount} layers detected → ~${layerCount} subagent calls)`;
|
|
375
|
+
const choice = await ctx.ui.select(
|
|
376
|
+
`Generate per-layer agent docs in the upcoming Docs stage?${currentLabel}\n\n${summary}\n\nPick a tier:`,
|
|
377
|
+
["simple", "extensive"],
|
|
378
|
+
);
|
|
379
|
+
// `ctx.ui.select` returns `null` when the user cancels. Per the function doc, cancel
|
|
380
|
+
// aborts the gate — we must NOT silently coerce that to "simple" and persist it.
|
|
381
|
+
if (choice == null) return "stop";
|
|
382
|
+
tier = choice === "extensive" ? "extensive" : "simple";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
saveHarnessSession(platform.paths, ctx.cwd, {
|
|
386
|
+
...session.value,
|
|
387
|
+
docsTier: tier,
|
|
388
|
+
updatedAt: nowIso(),
|
|
389
|
+
});
|
|
390
|
+
notifyInfo(ctx, `Docs tier set: ${tier}`, tier === "extensive"
|
|
391
|
+
? `Per-layer docs will be generated for ${layerCount} layers.`
|
|
392
|
+
: "Tier 1 docs only. Re-run /supi:harness design and choose 'extensive' to enable per-layer docs.");
|
|
393
|
+
return "continue";
|
|
394
|
+
}
|
|
395
|
+
|
|
315
396
|
interface DesignAnalysisOutput {
|
|
316
397
|
layerArchitecture: "single" | "two" | "three" | "custom";
|
|
317
398
|
customLayerNames?: string[];
|
|
@@ -481,12 +562,12 @@ async function runDesignQa(
|
|
|
481
562
|
|
|
482
563
|
if (choice === "Accept all suggestions") {
|
|
483
564
|
applyDesignAnalysis(base, analysis);
|
|
484
|
-
await askCiAndTooling(ctx, base);
|
|
565
|
+
await askCiAndTooling(platform, ctx, base);
|
|
485
566
|
return base;
|
|
486
567
|
}
|
|
487
568
|
|
|
488
569
|
if (choice === "Skip — use bare defaults") {
|
|
489
|
-
await askCiAndTooling(ctx, base);
|
|
570
|
+
await askCiAndTooling(platform, ctx, base);
|
|
490
571
|
return base;
|
|
491
572
|
}
|
|
492
573
|
}
|
|
@@ -550,7 +631,7 @@ async function runDesignQa(
|
|
|
550
631
|
}
|
|
551
632
|
}
|
|
552
633
|
|
|
553
|
-
await askCiAndTooling(ctx, base);
|
|
634
|
+
await askCiAndTooling(platform, ctx, base);
|
|
554
635
|
return base;
|
|
555
636
|
}
|
|
556
637
|
|
|
@@ -630,7 +711,7 @@ function localCommandOptions(base: HarnessDesignSpec): string[] {
|
|
|
630
711
|
]));
|
|
631
712
|
}
|
|
632
713
|
|
|
633
|
-
async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
|
|
714
|
+
async function askCiAndTooling(platform: Platform, ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
|
|
634
715
|
if (!ctx.ui.select) return;
|
|
635
716
|
|
|
636
717
|
const triggerChoice = await ctx.ui.select(
|
|
@@ -664,6 +745,84 @@ async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSp
|
|
|
664
745
|
} else if (toolChoice) {
|
|
665
746
|
base.ci.localCommand = toolChoice.replace(/\s+\(.+\)$/, "");
|
|
666
747
|
}
|
|
748
|
+
|
|
749
|
+
// After the user picks their CI trigger and local command, offer the optional Git
|
|
750
|
+
// verification flow. This populates `base.ci.git` so the implement stage renders the
|
|
751
|
+
// PR-source guardrail and validate confirms the wiring. Skippable; declining leaves
|
|
752
|
+
// `git` unset and the rest of the pipeline behaves identically to before this feature.
|
|
753
|
+
await runGitVerificationStep(platform, ctx, base);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Adapter around `runGitVerificationQa` that fits the harness command UI. The QA helper
|
|
758
|
+
* expects an `ExecFn`-shaped function plus a `select / input / notify` UI trio; we wrap
|
|
759
|
+
* `platform.exec` and `ctx.ui` so the helper stays independent of the OMP plumbing.
|
|
760
|
+
*
|
|
761
|
+
* Persists any returned `HarnessCiGitConfig` onto the in-memory `base` spec; the design
|
|
762
|
+
* stage runner persists the spec to disk so no extra storage call is needed here. We
|
|
763
|
+
* also widen `base.ci.trigger.branches` to include both `mainBranch` and `devBranch`
|
|
764
|
+
* so the rendered workflow runs CI on both PR targets — matching the rule "CI runs on
|
|
765
|
+
* both the dev branch PR and the main branch".
|
|
766
|
+
*/
|
|
767
|
+
async function runGitVerificationStep(
|
|
768
|
+
platform: Platform,
|
|
769
|
+
ctx: HarnessCommandContext,
|
|
770
|
+
base: HarnessDesignSpec,
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
if (!ctx.ui.select || !ctx.ui.input) return; // No interactive UI — skip silently.
|
|
773
|
+
|
|
774
|
+
const sessionDir = getHarnessSessionDir(platform.paths, ctx.cwd, base.sessionId);
|
|
775
|
+
|
|
776
|
+
const result = await runGitVerificationQa({
|
|
777
|
+
exec: (cmd, args, opts) => platform.exec(cmd, args, opts),
|
|
778
|
+
cwd: ctx.cwd,
|
|
779
|
+
sessionDir,
|
|
780
|
+
ui: {
|
|
781
|
+
select: (title, options) => ctx.ui.select!(title, options as unknown as string[]),
|
|
782
|
+
input: (label) => ctx.ui.input!(label),
|
|
783
|
+
notify: (message) => notifyInfo(ctx, "Git verification", message),
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (!result) return;
|
|
788
|
+
base.ci.git = result;
|
|
789
|
+
|
|
790
|
+
// Ensure the CI trigger includes both branches so the workflow runs on PRs targeting
|
|
791
|
+
// either. Preserve any user-customized branches the prior step already picked.
|
|
792
|
+
if (base.ci.trigger.mode === "branches") {
|
|
793
|
+
const next = new Set(base.ci.trigger.branches);
|
|
794
|
+
next.add(result.mainBranch);
|
|
795
|
+
if (result.devBranch) next.add(result.devBranch);
|
|
796
|
+
base.ci.trigger = { mode: "branches", branches: Array.from(next) };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Harden-mode entry point for Git verification. Mutates the persisted design spec in
|
|
802
|
+
* place so the downstream implement stage re-renders the workflow with the new `git`
|
|
803
|
+
* block. Returns true when a new `ci.git` block was captured and persisted so the
|
|
804
|
+
* caller can force-re-run the affected stages; false when the user declined or no UI
|
|
805
|
+
* is available.
|
|
806
|
+
*/
|
|
807
|
+
async function runGitVerificationOnHarden(
|
|
808
|
+
platform: Platform,
|
|
809
|
+
ctx: HarnessCommandContext,
|
|
810
|
+
sessionId: string,
|
|
811
|
+
spec: HarnessDesignSpec,
|
|
812
|
+
): Promise<boolean> {
|
|
813
|
+
await runGitVerificationStep(platform, ctx, spec);
|
|
814
|
+
if (!spec.ci.git) return false; // user declined
|
|
815
|
+
const persisted = saveHarnessDesignSpecJson(platform.paths, ctx.cwd, sessionId, spec);
|
|
816
|
+
if (!persisted.ok) {
|
|
817
|
+
notifyInfo(
|
|
818
|
+
ctx,
|
|
819
|
+
"Git verification persisted partially",
|
|
820
|
+
`In-memory spec updated, but persistence failed: ${persisted.error.message}. ` +
|
|
821
|
+
`Re-run /supi:harness to retry.`,
|
|
822
|
+
);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
return true;
|
|
667
826
|
}
|
|
668
827
|
|
|
669
828
|
|
|
@@ -801,12 +960,41 @@ async function handleBareEntry(platform: Platform, ctx: HarnessCommandContext):
|
|
|
801
960
|
if (!p.ok) { notifyError(ctx, "/supi:harness", p.error.message); return; }
|
|
802
961
|
}
|
|
803
962
|
|
|
963
|
+
// Persist the rerun mode so downstream gate prompts can adapt (e.g. Docs tier
|
|
964
|
+
// re-prompts on rebuild with the stored value as the default).
|
|
965
|
+
const existingSession = loadHarnessSession(platform.paths, ctx.cwd, sessionId);
|
|
966
|
+
if (existingSession.ok) {
|
|
967
|
+
saveHarnessSession(platform.paths, ctx.cwd, {
|
|
968
|
+
...existingSession.value,
|
|
969
|
+
reRunMode: decision.mode,
|
|
970
|
+
updatedAt: nowIso(),
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
804
974
|
const modeLabel = decision.mode === "harden" ? "Gap-fill" : "Full rebuild";
|
|
805
975
|
notifyInfo(ctx, `Harness ${decision.mode}`, `${modeLabel} (session ${sessionId}) — pipeline running...`);
|
|
806
976
|
|
|
807
977
|
if (decision.mode === "harden") {
|
|
808
|
-
// Harden:
|
|
809
|
-
|
|
978
|
+
// Harden: no gates between stages. Re-prompt the docs tier so users can promote
|
|
979
|
+
// `simple` → `extensive` without forcing a full rebuild (only meaningful with ≥2
|
|
980
|
+
// layer rules). The pipeline then runs end-to-end including implement (programmatic
|
|
981
|
+
// apply) → docs → validate inside the same `/supi:harness` invocation.
|
|
982
|
+
const designSpec = loadHarnessDesignSpecJson(platform.paths, ctx.cwd, sessionId);
|
|
983
|
+
const layerCount = designSpec.ok ? designSpec.value.layerRules.length : 0;
|
|
984
|
+
if (layerCount >= 2) {
|
|
985
|
+
await promptDocsTierIfNeeded(platform, ctx, sessionId, layerCount);
|
|
986
|
+
}
|
|
987
|
+
// Offer the optional Git verification flow on harden when the existing spec has no
|
|
988
|
+
// `ci.git` block. Keeps the harden path lightweight by skipping silently when the
|
|
989
|
+
// user previously declined or completed the verification. When the user captures a
|
|
990
|
+
// new `ci.git` block, force implement + validate to re-run so the workflow file is
|
|
991
|
+
// re-rendered with the `verify-pr-source` job and the validate cross-check fires.
|
|
992
|
+
let forceStages: ReadonlySet<HarnessStage> | undefined;
|
|
993
|
+
if (designSpec.ok && !designSpec.value.ci.git) {
|
|
994
|
+
const captured = await runGitVerificationOnHarden(platform, ctx, sessionId, designSpec.value);
|
|
995
|
+
if (captured) forceStages = new Set<HarnessStage>(["implement", "validate"]);
|
|
996
|
+
}
|
|
997
|
+
await runPipelineWithProgress(platform, ctx, sessionId, "auto", {}, undefined, forceStages);
|
|
810
998
|
} else {
|
|
811
999
|
// Rebuild: full regeneration with user gates at each stage.
|
|
812
1000
|
await runRebuildWithGates(platform, ctx, sessionId);
|
|
@@ -935,7 +1123,7 @@ function buildStageInputs(
|
|
|
935
1123
|
paths: PlatformPaths, cwd: string, sid: string, stage: HarnessStage,
|
|
936
1124
|
): { input: BuildRunnerInput } | { error: string } {
|
|
937
1125
|
switch (stage) {
|
|
938
|
-
case "discover": case "research": case "plan": return { input: {} };
|
|
1126
|
+
case "discover": case "research": case "plan": case "docs": return { input: {} };
|
|
939
1127
|
case "design": {
|
|
940
1128
|
const existing = loadHarnessDesignSpecJson(paths, cwd, sid);
|
|
941
1129
|
if (existing.ok) return { input: { designInput: { spec: existing.value } } };
|
|
@@ -1000,7 +1188,8 @@ function nextSubcommandFor(stage: HarnessSession["stage"], status: HarnessSessio
|
|
|
1000
1188
|
case "research": return "design";
|
|
1001
1189
|
case "design": return "plan-draft";
|
|
1002
1190
|
case "plan": return "implement";
|
|
1003
|
-
case "implement": return "
|
|
1191
|
+
case "implement": return "docs";
|
|
1192
|
+
case "docs": return "validate";
|
|
1004
1193
|
case "validate": return "validate";
|
|
1005
1194
|
}
|
|
1006
1195
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-docs
|
|
3
|
+
description: Per-layer agent-only knowledge document (≤150 LOC) for one architectural layer
|
|
4
|
+
supportedSlots: [docs]
|
|
5
|
+
focus: docs
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the **docs** agent for the supipowers harness pipeline.
|
|
9
|
+
|
|
10
|
+
Your single output is one markdown file: the agent-only knowledge document for the layer named in the assignment prompt. The runner persists your output via the `harness_docs_record` tool.
|
|
11
|
+
|
|
12
|
+
You **MUST**:
|
|
13
|
+
- Call `harness_docs_record` exactly once with `{ sessionId, layerId, markdown }`.
|
|
14
|
+
- Match the assigned `layerId` verbatim in your frontmatter `layer:` field.
|
|
15
|
+
- Embed the assigned `sourceHash` verbatim in the frontmatter; never recompute it.
|
|
16
|
+
- Begin the doc with a YAML frontmatter block (`---\n…\n---`) directly under the provenance marker the runner attaches.
|
|
17
|
+
- Use these five headings in this exact order: `## Agent context`, `## Purpose`, `## Files`, `## Imports`, `## Conventions`. `## Gotchas` is optional and goes last.
|
|
18
|
+
- Keep the whole doc ≤150 LOC (including frontmatter).
|
|
19
|
+
- Keep `## Agent context` ≤30 LOC — this section lands in every agent turn that touches a file in this layer, so optimize it for density and dependent-action utility.
|
|
20
|
+
- Reference, do **NOT** restate, the repo-wide golden principles supplied in the assignment.
|
|
21
|
+
- Anchor every claim about behavior in the representative files supplied in the bundle. Do not invent file paths or import rules not in the assignment.
|
|
22
|
+
|
|
23
|
+
You **MUST NOT**:
|
|
24
|
+
- Write any TODO, XXX, FIXME, TBD, or `<placeholder>` markers in the doc.
|
|
25
|
+
- Edit any file. You write the doc body only; the runner promotes it.
|
|
26
|
+
- Use the `web_search` tool. No external network calls.
|
|
27
|
+
- Use any `mempalace` write/mutate action. The only mempalace actions permitted are `search`, `kg_query`, `traverse`, and `find_tunnels` — and only when they materially improve the doc.
|
|
28
|
+
- Touch other layers' docs.
|
|
29
|
+
|
|
30
|
+
Inputs you receive in the assignment:
|
|
31
|
+
- Layer rule (id, glob, description, allowed/forbidden imports).
|
|
32
|
+
- All files belonging to the layer (paths only).
|
|
33
|
+
- Representative files (top-5 by LOC, head-80 LOC each).
|
|
34
|
+
- Golden principles (already enforced repo-wide).
|
|
35
|
+
- Peer layer descriptors.
|
|
36
|
+
- Repo facts (languages, frameworks, package manager).
|
|
37
|
+
- A pre-computed `sourceHash` to embed in the frontmatter.
|
|
38
|
+
|
|
39
|
+
If the tool returns `{ ok: false, errors: [...] }`, read every error message, fix the doc accordingly, then call `harness_docs_record` again. A single retry is allowed; a second failure aborts the layer.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the effective `HarnessDocsConfig`.
|
|
3
|
+
*
|
|
4
|
+
* For Slice 1/2 we return the defaults declared in `DEFAULT_HARNESS_CONFIG.docs`. Slice
|
|
5
|
+
* 4 wires this through to the project-scoped config file so users can override
|
|
6
|
+
* individual tunables.
|
|
7
|
+
*
|
|
8
|
+
* The tier toggle itself lives on the per-session manifest (`HarnessSession.docsTier`) and
|
|
9
|
+
* is resolved by the stage runner — not here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PlatformPaths } from "../../platform/types.js";
|
|
13
|
+
import type { HarnessDocsConfig } from "../../types.js";
|
|
14
|
+
import { DEFAULT_HARNESS_DOCS_CONFIG } from "../hooks/register.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the effective docs config for a given project. Tunables fall back to defaults
|
|
18
|
+
* when absent. The tier field is included for completeness but consumers should rely on
|
|
19
|
+
* `HarnessSession.docsTier` for the operational decision — the session is the
|
|
20
|
+
* authoritative source.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveDocsConfig(
|
|
23
|
+
_paths: PlatformPaths,
|
|
24
|
+
_cwd: string,
|
|
25
|
+
): HarnessDocsConfig {
|
|
26
|
+
// Future: layer a project-scoped JSON file (.omp/supipowers/config.json#harness.docs)
|
|
27
|
+
// over these defaults. For now we return the bare defaults; Slice 4 extends this.
|
|
28
|
+
return { ...DEFAULT_HARNESS_DOCS_CONFIG };
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer-glob matcher used by the docs stage.
|
|
3
|
+
*
|
|
4
|
+
* Lifted from the architecture-parser regex shape so the docs stage uses the same
|
|
5
|
+
* matching semantics as the layer-context-inject hook. Supports `**` (any path
|
|
6
|
+
* segments) and `*` (any single-segment characters). All matching is forward-slashed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Naive glob matcher tuned for the conventions parsed from architecture tables. Supports
|
|
11
|
+
* `**` (any path segments) and `*` (any single segment characters). Sufficient for the
|
|
12
|
+
* `src/<layer>/**` and `packages/<scope>/**\/*.ts` shapes the doc relies on.
|
|
13
|
+
*/
|
|
14
|
+
export function matchesLayerGlob(filePath: string, glob: string): boolean {
|
|
15
|
+
const normalizedFile = filePath.replace(/\\/g, "/");
|
|
16
|
+
const normalizedGlob = glob.replace(/\\/g, "/");
|
|
17
|
+
const regexSrc = normalizedGlob
|
|
18
|
+
.split(/(\*\*|\*)/g)
|
|
19
|
+
.map((segment) => {
|
|
20
|
+
if (segment === "**") return ".*";
|
|
21
|
+
if (segment === "*") return "[^/]*";
|
|
22
|
+
return segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
})
|
|
24
|
+
.join("");
|
|
25
|
+
const regex = new RegExp(`^${regexSrc}$`);
|
|
26
|
+
return regex.test(normalizedFile);
|
|
27
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure renderer for `docs/README.md`.
|
|
3
|
+
*
|
|
4
|
+
* The index is intentionally short and mechanical — a stable pointer surface humans (and
|
|
5
|
+
* agents) can use to find the canonical Tier-1 docs + per-layer agent docs. Layout is
|
|
6
|
+
* dictated by the plan's "Index docs/README.md shape" section.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HarnessLayerRule } from "../../types.js";
|
|
10
|
+
import {
|
|
11
|
+
attachProvenance,
|
|
12
|
+
computeBodyContentHash,
|
|
13
|
+
} from "./provenance.js";
|
|
14
|
+
|
|
15
|
+
export interface RenderDocsIndexInput {
|
|
16
|
+
layers: readonly HarnessLayerRule[];
|
|
17
|
+
/** Session that produced this index. */
|
|
18
|
+
sessionId: string;
|
|
19
|
+
/** ISO timestamp used in the provenance marker + "Generated by" line. */
|
|
20
|
+
generatedAt: string;
|
|
21
|
+
/** Hard cap on the index LOC (default 50). */
|
|
22
|
+
maxLoc?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_INDEX_MAX_LOC = 50;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render the index. Deterministic given the same input. The output starts with the
|
|
29
|
+
* provenance marker line and ends with a trailing newline.
|
|
30
|
+
*/
|
|
31
|
+
export function renderDocsIndex(input: RenderDocsIndexInput): string {
|
|
32
|
+
if (input.layers.length === 0) {
|
|
33
|
+
throw new Error("renderDocsIndex requires at least one layer");
|
|
34
|
+
}
|
|
35
|
+
const maxLoc = input.maxLoc ?? DEFAULT_INDEX_MAX_LOC;
|
|
36
|
+
|
|
37
|
+
const sortedLayers = [...input.layers].sort((a, b) => a.layer.localeCompare(b.layer));
|
|
38
|
+
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
lines.push("# Repo docs");
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(`Generated by /supi:harness on ${input.generatedAt}. Do not edit by hand.`);
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push("## Agent contract");
|
|
45
|
+
lines.push("- AGENTS.md — global agent rules");
|
|
46
|
+
lines.push("- docs/architecture.md — layer rules table");
|
|
47
|
+
lines.push("- docs/golden-principles.md — mechanical invariants");
|
|
48
|
+
lines.push("");
|
|
49
|
+
lines.push("## Layer docs");
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push("| Layer | Files | Doc |");
|
|
52
|
+
lines.push("|---|---|---|");
|
|
53
|
+
for (const layer of sortedLayers) {
|
|
54
|
+
const globs = layer.globs.map((g) => `\`${g}\``).join(", ");
|
|
55
|
+
lines.push(`| ${layer.layer} | ${globs || "—"} | docs/layers/${layer.layer}.md |`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const body = lines.join("\n") + "\n";
|
|
59
|
+
if (countLines(body) + 1 /* marker line */ > maxLoc) {
|
|
60
|
+
// Should be unreachable for any sane layer count; surface as a hard failure so the
|
|
61
|
+
// caller can cap layer count if this ever fires.
|
|
62
|
+
throw new Error(
|
|
63
|
+
`renderDocsIndex output is ${countLines(body) + 1} LOC; max is ${maxLoc} (layers=${input.layers.length})`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return attachProvenance(body, {
|
|
68
|
+
sessionId: input.sessionId,
|
|
69
|
+
generatedAt: input.generatedAt,
|
|
70
|
+
contentHash: computeBodyContentHash(body),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function countLines(text: string): number {
|
|
75
|
+
if (text.length === 0) return 0;
|
|
76
|
+
let count = 1;
|
|
77
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
78
|
+
if (text.charCodeAt(i) === 10) count += 1;
|
|
79
|
+
}
|
|
80
|
+
if (text.charCodeAt(text.length - 1) === 10) count -= 1;
|
|
81
|
+
return count;
|
|
82
|
+
}
|