omegon 0.6.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 (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,65 @@
1
+ import type { ChangeInfo } from "./types.ts";
2
+ import type { LifecycleMemoryCandidate } from "../project-memory/types.ts";
3
+
4
+ export function emitArchiveCandidates(change: ChangeInfo): LifecycleMemoryCandidate[] {
5
+ if (change.stage !== "archived") return [];
6
+
7
+ const candidates: LifecycleMemoryCandidate[] = [];
8
+ for (const spec of change.specs) {
9
+ for (const section of spec.sections) {
10
+ if (section.type === "removed") continue;
11
+ for (const requirement of section.requirements) {
12
+ candidates.push({
13
+ sourceKind: "openspec-archive",
14
+ authority: "explicit",
15
+ section: "Specs",
16
+ content: `${requirement.title} (${section.type})`,
17
+ artifactRef: {
18
+ type: "openspec-baseline",
19
+ path: `openspec/baseline/${spec.domain}.md`,
20
+ subRef: requirement.title,
21
+ },
22
+ });
23
+ }
24
+ }
25
+ }
26
+ return candidates;
27
+ }
28
+
29
+ export function emitReconcileCandidates(changeName: string, summary?: string, constraints?: string[]): LifecycleMemoryCandidate[] {
30
+ const candidates: LifecycleMemoryCandidate[] = [];
31
+
32
+ for (const constraint of constraints ?? []) {
33
+ const trimmed = constraint.trim();
34
+ if (!trimmed || /\b(might|could|should|consider|possible|likely)\b/i.test(trimmed) || /\?$/.test(trimmed)) {
35
+ continue;
36
+ }
37
+ candidates.push({
38
+ sourceKind: "openspec-assess",
39
+ authority: "explicit",
40
+ section: "Constraints",
41
+ content: trimmed,
42
+ artifactRef: {
43
+ type: "openspec-spec",
44
+ path: `openspec/changes/${changeName}/tasks.md`,
45
+ subRef: "reconcile_after_assess",
46
+ },
47
+ });
48
+ }
49
+
50
+ if (summary && /\b(fixed|resolved|workaround)\b/i.test(summary)) {
51
+ candidates.push({
52
+ sourceKind: "openspec-assess",
53
+ authority: "explicit",
54
+ section: "Known Issues",
55
+ content: summary.trim(),
56
+ artifactRef: {
57
+ type: "openspec-spec",
58
+ path: `openspec/changes/${changeName}/tasks.md`,
59
+ subRef: "reconcile_after_assess",
60
+ },
61
+ });
62
+ }
63
+
64
+ return candidates;
65
+ }
@@ -0,0 +1,70 @@
1
+ import * as path from "node:path";
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const DURABLE_ROOTS = ["docs", "openspec"] as const;
6
+
7
+ export interface LifecycleArtifactCheckResult {
8
+ untracked: string[];
9
+ }
10
+
11
+ export function isDurableLifecycleArtifact(filePath: string): boolean {
12
+ const normalized = filePath.replaceAll("\\", "/").replace(/^\.\//, "");
13
+ return DURABLE_ROOTS.some((root) => normalized === root || normalized.startsWith(`${root}/`));
14
+ }
15
+
16
+ export function parsePorcelainZ(stdout: string): string[] {
17
+ const entries = stdout.split("\0").filter(Boolean);
18
+ const untracked: string[] = [];
19
+ for (const entry of entries) {
20
+ if (entry.startsWith("?? ")) {
21
+ untracked.push(entry.slice(3));
22
+ }
23
+ }
24
+ return untracked;
25
+ }
26
+
27
+ export function detectUntrackedLifecycleArtifacts(repoPath: string): string[] {
28
+ try {
29
+ const stdout = execFileSync(
30
+ "git",
31
+ ["status", "--porcelain", "--untracked-files=all", "-z", "--", ...DURABLE_ROOTS],
32
+ { cwd: repoPath, encoding: "utf-8" },
33
+ );
34
+ return parsePorcelainZ(stdout)
35
+ .filter(isDurableLifecycleArtifact)
36
+ .sort((a, b) => a.localeCompare(b));
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ export function formatLifecycleArtifactError(result: LifecycleArtifactCheckResult): string {
43
+ const lines = [
44
+ "Untracked durable lifecycle artifacts detected.",
45
+ "",
46
+ "The following files live under docs/ or openspec/ and are treated as version-controlled project documentation:",
47
+ ...result.untracked.map((file) => `- ${file}`),
48
+ "",
49
+ "Resolution:",
50
+ "- git add the durable lifecycle files listed above, or",
51
+ "- move transient scratch artifacts outside docs/ and openspec/.",
52
+ ];
53
+ return lines.join("\n");
54
+ }
55
+
56
+ export function assertTrackedLifecycleArtifacts(repoPath: string): void {
57
+ const untracked = detectUntrackedLifecycleArtifacts(repoPath);
58
+ if (untracked.length === 0) return;
59
+ throw new Error(formatLifecycleArtifactError({ untracked }));
60
+ }
61
+
62
+ function runCli(): void {
63
+ const repoPath = process.cwd();
64
+ assertTrackedLifecycleArtifacts(repoPath);
65
+ }
66
+
67
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
68
+ if (isMain) {
69
+ runCli();
70
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * openspec/lifecycle — Canonical lifecycle resolver for OpenSpec surfaces.
3
+ *
4
+ * Exports buildLifecycleSummary, the single shared function that both the
5
+ * status and get surfaces in index.ts call to derive stage, verificationSubstate,
6
+ * and archiveReady. Extracting it here makes it importable by tests so that
7
+ * cross-surface agreement can be asserted against the actual production logic.
8
+ *
9
+ * No pi dependency — can be tested standalone.
10
+ */
11
+
12
+ import type { ChangeInfo } from "./types.ts";
13
+ import {
14
+ getAssessmentStatus,
15
+ resolveLifecycleSummary,
16
+ type LifecycleSummary,
17
+ } from "./spec.ts";
18
+ import { evaluateLifecycleReconciliation } from "./reconcile.ts";
19
+
20
+ export type { LifecycleSummary };
21
+
22
+ /**
23
+ * Derive the canonical lifecycle summary for a change.
24
+ *
25
+ * This is the single source of truth consumed by:
26
+ * - the `status` action (list view)
27
+ * - the `get` action (detail view)
28
+ * - the `archive` gate
29
+ * - dashboard-state publication
30
+ *
31
+ * All surfaces must call this function rather than re-deriving lifecycle
32
+ * state locally so that they cannot diverge.
33
+ */
34
+ export function buildLifecycleSummary(repoPath: string, change: ChangeInfo): LifecycleSummary {
35
+ const assessment = getAssessmentStatus(repoPath, change.name);
36
+ const reconciliation = evaluateLifecycleReconciliation(repoPath, change.name);
37
+ const blockingIssues = reconciliation.issues.filter((issue) => issue.isError !== false);
38
+ const archiveBlockedReason = blockingIssues.length > 0
39
+ ? blockingIssues.map((issue) => issue.suggestedAction).join(" ")
40
+ : null;
41
+ return resolveLifecycleSummary({
42
+ change,
43
+ record: assessment.record,
44
+ freshness: assessment.freshness,
45
+ archiveBlocked: blockingIssues.length > 0,
46
+ archiveBlockedReason,
47
+ archiveBlockedIssueCodes: blockingIssues.map((issue) => issue.code),
48
+ boundNodeIds: reconciliation.boundNodeIds,
49
+ });
50
+ }
@@ -0,0 +1,187 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ import {
5
+ getNodeSections,
6
+ writeNodeDocument,
7
+ } from "../design-tree/tree.ts";
8
+ import type { DesignNode, FileScope } from "../design-tree/types.ts";
9
+ import { resolveBoundDesignNodes } from "./archive-gate.ts";
10
+ import { getChange } from "./spec.ts";
11
+
12
+ export type ReconciliationIssueCode = "incomplete_tasks" | "missing_design_binding";
13
+ export type PostAssessOutcome = "pass" | "reopen" | "ambiguous";
14
+ export type AssessmentKind = "spec" | "cleave";
15
+
16
+ export interface ReconciliationIssue {
17
+ code: ReconciliationIssueCode;
18
+ message: string;
19
+ suggestedAction: string;
20
+ isError?: boolean;
21
+ severity?: "error" | "warning" | "info";
22
+ }
23
+
24
+ export interface LifecycleReconciliationStatus {
25
+ changeName: string;
26
+ boundNodeIds: string[];
27
+ issues: ReconciliationIssue[];
28
+ }
29
+
30
+ export interface PostAssessReconciliationInput {
31
+ assessmentKind: AssessmentKind;
32
+ outcome: PostAssessOutcome;
33
+ summary?: string;
34
+ changedFiles?: string[];
35
+ constraints?: string[];
36
+ }
37
+
38
+ export interface PostAssessReconciliationResult {
39
+ changeName: string;
40
+ outcome: PostAssessOutcome;
41
+ reopened: boolean;
42
+ warning?: string;
43
+ updatedTaskState: boolean;
44
+ updatedNodeIds: string[];
45
+ appendedFileScope: string[];
46
+ appendedConstraints: string[];
47
+ }
48
+
49
+ function findBoundNodes(cwd: string, changeName: string): DesignNode[] {
50
+ return resolveBoundDesignNodes(cwd, changeName);
51
+ }
52
+
53
+ export function evaluateLifecycleReconciliation(cwd: string, changeName: string): LifecycleReconciliationStatus {
54
+ const issues: ReconciliationIssue[] = [];
55
+ const change = getChange(cwd, changeName);
56
+ const boundNodes = findBoundNodes(cwd, changeName);
57
+
58
+ if (boundNodes.length === 0) {
59
+ issues.push({
60
+ code: "missing_design_binding",
61
+ message: `OpenSpec change '${changeName}' is not bound to any design-tree node via openspec_change or matching node ID. (Legacy node — binding is guaranteed by construction for new nodes created via design_tree_update implement.)`,
62
+ suggestedAction: "Bind the change to a decided/implementing design node before archive so lifecycle tracking stays traceable.",
63
+ isError: false,
64
+ severity: "info",
65
+ });
66
+ }
67
+
68
+ if (change && change.hasTasks && change.totalTasks > 0 && change.doneTasks < change.totalTasks) {
69
+ issues.push({
70
+ code: "incomplete_tasks",
71
+ message: `OpenSpec change '${changeName}' still has ${change.totalTasks - change.doneTasks} incomplete task(s) in tasks.md.`,
72
+ suggestedAction: "Reconcile tasks.md to match implemented work or finish the remaining tasks before archive.",
73
+ });
74
+ }
75
+
76
+ return {
77
+ changeName,
78
+ boundNodeIds: boundNodes.map((node) => node.id),
79
+ issues,
80
+ };
81
+ }
82
+
83
+ function appendPostAssessFollowUpTask(changePath: string, input: PostAssessReconciliationInput): boolean {
84
+ const tasksPath = path.join(changePath, "tasks.md");
85
+ if (!fs.existsSync(tasksPath)) return false;
86
+
87
+ const content = fs.readFileSync(tasksPath, "utf-8");
88
+ const summary = (input.summary || `Resolve remaining findings from /assess ${input.assessmentKind}`).trim();
89
+ const escapedSummary = summary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
90
+ const existing = new RegExp(`^\\s*-\\s+\\[ \\]\\s+\\d+(?:\\.\\d+)?\\s+${escapedSummary}$`, "m");
91
+ if (existing.test(content)) return false;
92
+
93
+ const groupNumbers = [...content.matchAll(/^##\s+(\d+)\.\s+/gm)].map((m) => parseInt(m[1], 10));
94
+ const nextGroup = groupNumbers.length > 0 ? Math.max(...groupNumbers) + 1 : 1;
95
+ const nextTask = `${nextGroup}.1`;
96
+ const block = [
97
+ "",
98
+ `## ${nextGroup}. Post-assess follow-up`,
99
+ "<!-- skills: typescript -->",
100
+ `- [ ] ${nextTask} ${summary}`,
101
+ ].join("\n");
102
+
103
+ fs.writeFileSync(tasksPath, content.replace(/\s*$/, "") + block + "\n", "utf-8");
104
+ return true;
105
+ }
106
+
107
+ function appendImplementationDeltas(
108
+ node: DesignNode,
109
+ changedFiles: readonly string[],
110
+ constraints: readonly string[],
111
+ ): { appendedFiles: string[]; appendedConstraints: string[] } {
112
+ const sections = getNodeSections(node);
113
+ const existingFiles = new Set(sections.implementationNotes.fileScope.map((entry) => entry.path));
114
+ const existingConstraints = new Set(sections.implementationNotes.constraints);
115
+
116
+ const newFileScope: FileScope[] = [];
117
+ for (const file of changedFiles) {
118
+ if (!existingFiles.has(file)) {
119
+ newFileScope.push({
120
+ path: file,
121
+ description: "Post-assess reconciliation delta — touched during follow-up fixes",
122
+ action: "modified",
123
+ });
124
+ }
125
+ }
126
+
127
+ const newConstraints = constraints.filter((constraint) => !existingConstraints.has(constraint));
128
+ if (newFileScope.length === 0 && newConstraints.length === 0) {
129
+ return { appendedFiles: [], appendedConstraints: [] };
130
+ }
131
+
132
+ sections.implementationNotes.fileScope.push(...newFileScope);
133
+ sections.implementationNotes.constraints.push(...newConstraints);
134
+ writeNodeDocument(node, sections);
135
+
136
+ return {
137
+ appendedFiles: newFileScope.map((entry) => entry.path),
138
+ appendedConstraints: newConstraints,
139
+ };
140
+ }
141
+
142
+ export function applyPostAssessReconciliation(
143
+ cwd: string,
144
+ changeName: string,
145
+ input: PostAssessReconciliationInput,
146
+ ): PostAssessReconciliationResult {
147
+ const change = getChange(cwd, changeName);
148
+ const boundNodes = findBoundNodes(cwd, changeName);
149
+ const changedFiles = (input.changedFiles ?? []).map((file) => file.trim()).filter(Boolean);
150
+ const constraints = (input.constraints ?? []).map((constraint) => constraint.trim()).filter(Boolean);
151
+
152
+ let updatedTaskState = false;
153
+ let warning: string | undefined;
154
+ const updatedNodeIds: string[] = [];
155
+ const appendedFileScope: string[] = [];
156
+ const appendedConstraints: string[] = [];
157
+
158
+ if (input.outcome === "ambiguous") {
159
+ warning = "Post-assess reconciliation could not safely map reviewer prose back into OpenSpec tasks. No semantic task rewriting was attempted.";
160
+ } else if (input.outcome === "reopen" && change) {
161
+ updatedTaskState = appendPostAssessFollowUpTask(change.path, input);
162
+ }
163
+
164
+ for (const node of boundNodes) {
165
+ const delta = appendImplementationDeltas(node, changedFiles, constraints);
166
+ if (delta.appendedFiles.length > 0 || delta.appendedConstraints.length > 0) {
167
+ updatedNodeIds.push(node.id);
168
+ appendedFileScope.push(...delta.appendedFiles);
169
+ appendedConstraints.push(...delta.appendedConstraints);
170
+ }
171
+ }
172
+
173
+ return {
174
+ changeName,
175
+ outcome: input.outcome,
176
+ reopened: input.outcome === "reopen",
177
+ warning,
178
+ updatedTaskState,
179
+ updatedNodeIds,
180
+ appendedFileScope,
181
+ appendedConstraints,
182
+ };
183
+ }
184
+
185
+ export function formatReconciliationIssues(issues: readonly ReconciliationIssue[]): string {
186
+ return issues.map((issue) => `- ${issue.message}\n → ${issue.suggestedAction}`).join("\n");
187
+ }