omegon 0.7.8 → 0.8.1
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 +8 -4
- package/extensions/cleave/dispatcher.ts +1 -1
- package/extensions/cleave/index.ts +1 -0
- package/extensions/cleave/workspace.ts +4 -3
- package/extensions/dashboard/footer.ts +84 -21
- package/extensions/design-tree/index.ts +87 -56
- package/extensions/design-tree/tree.ts +113 -0
- package/extensions/lib/shared-state.ts +24 -0
- package/extensions/openspec/index.ts +25 -0
- package/extensions/project-memory/factstore.ts +176 -63
- package/extensions/project-memory/index.ts +91 -0
- package/extensions/spinner-verbs.ts +167 -37
- package/package.json +2 -3
- package/extensions/clipboard-diag/index.ts +0 -64
- package/extensions/distill.ts +0 -127
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ omegon # start Omegon in any project directory
|
|
|
42
42
|
|
|
43
43
|

|
|
44
44
|
|
|
45
|
-
Omegon extends `@styrene-lab/pi-coding-agent` with **
|
|
45
|
+
Omegon extends `@styrene-lab/pi-coding-agent` with **31 extensions**, **12 skills**, and **4 prompt templates** — loaded automatically on session start.
|
|
46
46
|
|
|
47
47
|
### Development Methodology
|
|
48
48
|
|
|
@@ -87,8 +87,10 @@ Structured design exploration with persistent markdown documents — the upstrea
|
|
|
87
87
|
- **Blocked audit**: `design_tree(action="blocked")` returns all stalled nodes with each blocking dependency's id, title, and status
|
|
88
88
|
- **Priority**: `set_priority` (1 = critical → 5 = trivial) on any node; `ready` auto-sorts by it
|
|
89
89
|
- **Issue types**: `set_issue_type` classifies nodes as `epic | feature | task | bug | chore` — bugs and chores are now first-class tracked work
|
|
90
|
-
- **
|
|
91
|
-
- **
|
|
90
|
+
- **Auto-transition**: Adding research or decisions to a `seed` node automatically transitions it to `exploring` and scaffolds the design spec — no manual ceremony
|
|
91
|
+
- **Substance-over-ceremony gates**: `set_status(decided)` checks for open questions and recorded decisions instead of artifact directory existence. Design specs are auto-extracted from doc content and archived
|
|
92
|
+
- **OpenSpec bridge**: `design_tree_update` with `action: "implement"` scaffolds `openspec/changes/<node-id>/` from a decided node's content, auto-checkouts the directive branch, forks a scoped memory mind, and sets design focus; `/cleave` executes it
|
|
93
|
+
- **Full pipeline**: design → decide → implement (auto-checkout + mind fork) → `/cleave` → `/assess spec` → archive (mind merge-back + cleanup)
|
|
92
94
|
|
|
93
95
|
### 🧠 Project Memory
|
|
94
96
|
|
|
@@ -98,8 +100,9 @@ Persistent, cross-session knowledge stored in SQLite. Accumulates architectural
|
|
|
98
100
|
- **Semantic retrieval**: Embedding-based search via Ollama (`qwen3-embedding`), falls back to FTS5 keyword search
|
|
99
101
|
- **Background extraction**: Auto-discovers facts from tool output without interrupting work
|
|
100
102
|
- **Episodic memory**: Generates session narratives at shutdown for "what happened last time" context
|
|
103
|
+
- **Directive minds**: `implement` forks a scoped memory mind from `default`; all fact reads/writes auto-scope to the directive. `archive` ingests discoveries back to `default` and cleans up. Zero-copy fork with parent-chain inheritance — no fact duplication, parent embeddings and edges are reused
|
|
101
104
|
- **Global knowledge base**: Cross-project facts at `~/.pi/memory/global.db`
|
|
102
|
-
- **Git sync**: Exports to JSONL for version-controlled knowledge sharing across machines
|
|
105
|
+
- **Git sync**: Exports to JSONL for version-controlled knowledge sharing across machines; volatile runtime scoring metadata (confidence, reinforcement counts, decay scores) omitted from exports for stable diffs
|
|
103
106
|
|
|
104
107
|

|
|
105
108
|
|
|
@@ -112,6 +115,7 @@ Live status panel showing design tree, OpenSpec changes, cleave dispatch, and gi
|
|
|
112
115
|
- **Raised mode**: Full-width expanded view (toggle with `/dash`)
|
|
113
116
|
- Git branch tree rooted at repo name, annotated with linked design nodes
|
|
114
117
|
- Two-column split at ≥120 terminal columns: design tree + cleave left, OpenSpec right
|
|
118
|
+
- Directive indicator: shows `▸ directive: name ✓` (branch match) or `▸ directive: name ⚠ main` (mismatch) when a directive mind is active
|
|
115
119
|
- Context gauge · model · thinking level in shared footer zone
|
|
116
120
|
- No line cap — renders as much content as needed
|
|
117
121
|
- **Keyboard**: `Ctrl+Shift+B` toggles raised/compact
|
|
@@ -294,7 +294,7 @@ export function buildChildPrompt(
|
|
|
294
294
|
" - Fill in Summary, Artifacts, Decisions Made, Interfaces Published",
|
|
295
295
|
"3. **Commits**: Commit your work with clear messages. Do not push.",
|
|
296
296
|
"4. **No side effects**: Do not install global packages or modify system state.",
|
|
297
|
-
"5. **
|
|
297
|
+
"5. **Testing**: Write tests for new functions and changed behavior in co-located *.test.ts files. Run them and report results in the Verification section. Untested code is incomplete.",
|
|
298
298
|
`6. **Workspace**: ${workspacePath}`,
|
|
299
299
|
];
|
|
300
300
|
|
|
@@ -1583,6 +1583,7 @@ export default function cleaveExtension(pi: ExtensionAPI) {
|
|
|
1583
1583
|
promptSnippet:
|
|
1584
1584
|
"Assess task complexity for decomposition — returns pattern match, complexity score, and execute/cleave decision",
|
|
1585
1585
|
promptGuidelines: [
|
|
1586
|
+
"Every non-trivial code change must include tests in co-located *.test.ts files. Untested code is incomplete — do not commit without tests for new functions and changed behavior.",
|
|
1586
1587
|
"Call cleave_assess before starting any multi-system or cross-cutting task to determine if decomposition is needed",
|
|
1587
1588
|
"If decision is 'execute', proceed directly. If 'cleave', use /cleave to decompose. If 'needs_assessment', proceed directly — it means no pattern matched but the task is likely simple enough for in-session execution.",
|
|
1588
1589
|
"Complexity formula: (1 + systems) × (1 + 0.5 × modifiers). Threshold default: 2.0.",
|
|
@@ -238,9 +238,10 @@ ${skillSection}${designSection}${guardrailSection ?? ""}
|
|
|
238
238
|
## Contract
|
|
239
239
|
|
|
240
240
|
1. Only work on files within your scope
|
|
241
|
-
2.
|
|
242
|
-
3.
|
|
243
|
-
4.
|
|
241
|
+
2. Write tests for new functions and changed behavior — co-locate as *.test.ts
|
|
242
|
+
3. Update the Result section below when done
|
|
243
|
+
4. Commit your work with clear messages — do not push
|
|
244
|
+
5. If the task is too complex, set status to NEEDS_DECOMPOSITION
|
|
244
245
|
|
|
245
246
|
## Result
|
|
246
247
|
|
|
@@ -546,27 +546,33 @@ export class DashboardFooter implements Component {
|
|
|
546
546
|
// ── Provider / model / thinking line ──────────────────────
|
|
547
547
|
const m = ctx.model;
|
|
548
548
|
if (m) {
|
|
549
|
-
const multiProvider = this.footerData.getAvailableProviderCount() > 1;
|
|
550
549
|
const pointer = theme.fg("accent", "▸");
|
|
551
|
-
const dot = theme.fg("dim", "
|
|
550
|
+
const dot = theme.fg("dim", " · ");
|
|
552
551
|
|
|
552
|
+
// Width-aware: narrow cards drop provider prefix, abbreviate thinking
|
|
553
|
+
const narrow = width < 40;
|
|
554
|
+
const multiProvider = !narrow && this.footerData.getAvailableProviderCount() > 1;
|
|
555
|
+
|
|
556
|
+
// Shorten model ID for display: "claude-opus-4-6" stays, but drop provider/ prefix if present in ID
|
|
557
|
+
const modelDisplay = m.id.replace(/^.*\//, "");
|
|
553
558
|
const providerModel = multiProvider
|
|
554
|
-
? `${pointer} ${theme.fg("muted", m.provider)}
|
|
555
|
-
: `${pointer} ${theme.fg("muted",
|
|
559
|
+
? `${pointer} ${theme.fg("muted", m.provider)}${theme.fg("dim", "/")}${theme.fg("muted", modelDisplay)}`
|
|
560
|
+
: `${pointer} ${theme.fg("muted", modelDisplay)}`;
|
|
556
561
|
|
|
557
562
|
const parts: string[] = [providerModel];
|
|
558
563
|
|
|
559
|
-
if (m.reasoning) {
|
|
564
|
+
if (m.reasoning && this.cachedThinkingLevel) {
|
|
560
565
|
const thinkColor: ThemeColor = this.cachedThinkingLevel === "high" ? "accent"
|
|
561
566
|
: this.cachedThinkingLevel === "medium" ? "muted"
|
|
562
567
|
: "dim";
|
|
563
568
|
const thinkIcon = this.cachedThinkingLevel === "off"
|
|
564
569
|
? theme.fg("dim", "○")
|
|
565
570
|
: theme.fg(thinkColor, "◉");
|
|
566
|
-
|
|
571
|
+
// Narrow: icon only, no label
|
|
572
|
+
parts.push(narrow ? thinkIcon : `${thinkIcon} ${theme.fg(thinkColor, this.cachedThinkingLevel)}`);
|
|
567
573
|
}
|
|
568
574
|
|
|
569
|
-
if (this.cachedTokens.cost > 0) {
|
|
575
|
+
if (this.cachedTokens.cost > 0 && !narrow) {
|
|
570
576
|
parts.push(theme.fg("dim", `$${this.cachedTokens.cost.toFixed(3)}`));
|
|
571
577
|
}
|
|
572
578
|
|
|
@@ -592,7 +598,8 @@ export class DashboardFooter implements Component {
|
|
|
592
598
|
|
|
593
599
|
if (!metrics && totalFacts === null) return "";
|
|
594
600
|
|
|
595
|
-
const sep = theme.fg("dim", "
|
|
601
|
+
const sep = theme.fg("dim", " · ");
|
|
602
|
+
const narrow = width < 35;
|
|
596
603
|
const parts: string[] = [];
|
|
597
604
|
|
|
598
605
|
if (totalFacts !== null) {
|
|
@@ -600,17 +607,24 @@ export class DashboardFooter implements Component {
|
|
|
600
607
|
}
|
|
601
608
|
|
|
602
609
|
if (metrics) {
|
|
610
|
+
// Always show injection count and token estimate
|
|
603
611
|
if (metrics.projectFactCount > 0)
|
|
604
612
|
parts.push(theme.fg("dim", "inj ") + theme.fg("muted", String(metrics.projectFactCount)));
|
|
605
|
-
|
|
613
|
+
// Show working memory and episodes only when there's room
|
|
614
|
+
if (!narrow && metrics.workingMemoryFactCount > 0)
|
|
606
615
|
parts.push(theme.fg("dim", "wm ") + theme.fg("muted", String(metrics.workingMemoryFactCount)));
|
|
607
|
-
if (metrics.episodeCount > 0)
|
|
616
|
+
if (!narrow && metrics.episodeCount > 0)
|
|
608
617
|
parts.push(theme.fg("dim", "ep ") + theme.fg("muted", String(metrics.episodeCount)));
|
|
609
|
-
|
|
618
|
+
// Drop global count in narrow — least useful
|
|
619
|
+
if (!narrow && metrics.globalFactCount > 0)
|
|
610
620
|
parts.push(theme.fg("dim", "gl ") + theme.fg("muted", String(metrics.globalFactCount)));
|
|
611
|
-
|
|
621
|
+
// Token estimate — always show, use compact format
|
|
622
|
+
const tokStr = metrics.estimatedTokens >= 1000
|
|
623
|
+
? `~${(metrics.estimatedTokens / 1000).toFixed(1)}k`
|
|
624
|
+
: `~${metrics.estimatedTokens}`;
|
|
625
|
+
parts.push(theme.fg("dim", tokStr));
|
|
612
626
|
} else {
|
|
613
|
-
parts.push(theme.fg("dim", "pending
|
|
627
|
+
parts.push(theme.fg("dim", "pending"));
|
|
614
628
|
}
|
|
615
629
|
|
|
616
630
|
return truncateToWidth(` ${parts.join(sep)}`, width, "…");
|
|
@@ -674,6 +688,41 @@ export class DashboardFooter implements Component {
|
|
|
674
688
|
return lines;
|
|
675
689
|
}
|
|
676
690
|
|
|
691
|
+
/**
|
|
692
|
+
* Derive a short directive label from the active mind name.
|
|
693
|
+
* "directive/my-feature" → "my-feature", null/default → null.
|
|
694
|
+
*/
|
|
695
|
+
private getDirectiveLabel(): string | null {
|
|
696
|
+
const mind = sharedState.activeMind;
|
|
697
|
+
if (!mind || mind === "default") return null;
|
|
698
|
+
// Strip common prefixes: "directive/", "mind/"
|
|
699
|
+
const label = mind.replace(/^(?:directive|mind)\//, "");
|
|
700
|
+
return label || null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Build directive indicator lines for the model card area.
|
|
705
|
+
* Shows "▸ directive: my-feature ✓" (branch match) or "▸ directive: my-feature ⚠ main" (mismatch).
|
|
706
|
+
* Returns empty array when no directive is active.
|
|
707
|
+
*/
|
|
708
|
+
private buildDirectiveIndicatorLines(width: number): string[] {
|
|
709
|
+
const label = this.getDirectiveLabel();
|
|
710
|
+
if (!label) return [];
|
|
711
|
+
const theme = this.theme;
|
|
712
|
+
const directive = sharedState.activeDirective;
|
|
713
|
+
const currentBranch = this.footerData.getGitBranch?.() ?? "";
|
|
714
|
+
|
|
715
|
+
let branchBadge = "";
|
|
716
|
+
if (directive && currentBranch) {
|
|
717
|
+
branchBadge = currentBranch === directive.branch
|
|
718
|
+
? ` ${theme.fg("success", "✓")}`
|
|
719
|
+
: ` ${theme.fg("error", "⚠")} ${theme.fg("dim", currentBranch)}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const line = `${theme.fg("warning", "▸")} ${theme.fg("dim", "directive:")} ${theme.fg("warning", label)}${branchBadge}`;
|
|
723
|
+
return [truncateToWidth(line, width, "…")];
|
|
724
|
+
}
|
|
725
|
+
|
|
677
726
|
private buildModelTopologySummaries(): DashboardModelRoleSummary[] {
|
|
678
727
|
const ctx = this.ctxRef;
|
|
679
728
|
const summaries: DashboardModelRoleSummary[] = [];
|
|
@@ -751,8 +800,12 @@ export class DashboardFooter implements Component {
|
|
|
751
800
|
: theme.fg("dim", summary.state));
|
|
752
801
|
const normalized = normalizeLocalModelLabel(summary.model);
|
|
753
802
|
const alias = forceCompact ? "" : (normalized.alias ? theme.fg("dim", `alias ${normalized.alias}`) : "");
|
|
754
|
-
|
|
755
|
-
const
|
|
803
|
+
// In very compact mode, drop the role label to give more room to the model name
|
|
804
|
+
const veryCompact = width < 30;
|
|
805
|
+
const roleLabel = veryCompact ? "" : (forceCompact ? summary.label.slice(0, 1) : summary.label);
|
|
806
|
+
const primary = roleLabel
|
|
807
|
+
? `${theme.fg("accent", roleLabel)} ${theme.fg("muted", normalized.canonical)}`
|
|
808
|
+
: theme.fg("muted", normalized.canonical);
|
|
756
809
|
return truncateToWidth(
|
|
757
810
|
composePrimaryMetaLine(
|
|
758
811
|
width,
|
|
@@ -790,7 +843,10 @@ export class DashboardFooter implements Component {
|
|
|
790
843
|
contextCard: this.buildSummaryCard("context", this.buildHudContextLines(Math.max(1, safeWidth - 2)).map((l) => l.trimStart()), safeWidth),
|
|
791
844
|
modelCard: this.buildSummaryCard(
|
|
792
845
|
"models",
|
|
793
|
-
|
|
846
|
+
[
|
|
847
|
+
...this.buildDirectiveIndicatorLines(Math.max(1, safeWidth - 2)),
|
|
848
|
+
...this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, safeWidth - 2), safeWidth < 44)),
|
|
849
|
+
],
|
|
794
850
|
safeWidth,
|
|
795
851
|
),
|
|
796
852
|
memoryCard: this.buildSummaryCard("memory", (() => {
|
|
@@ -852,7 +908,10 @@ export class DashboardFooter implements Component {
|
|
|
852
908
|
contextCard: this.buildSummaryCardForColumn("context", this.buildHudContextLines(Math.max(1, col1W - 2)).map((l) => l.trimStart()), col1W, col1W),
|
|
853
909
|
modelCard: this.buildSummaryCardForColumn(
|
|
854
910
|
"models",
|
|
855
|
-
|
|
911
|
+
[
|
|
912
|
+
...this.buildDirectiveIndicatorLines(Math.max(1, col2W - 2)),
|
|
913
|
+
...this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, col2W - 2), col2W < 44)),
|
|
914
|
+
],
|
|
856
915
|
col2W,
|
|
857
916
|
col2W,
|
|
858
917
|
),
|
|
@@ -1074,10 +1133,14 @@ export class DashboardFooter implements Component {
|
|
|
1074
1133
|
}
|
|
1075
1134
|
}
|
|
1076
1135
|
|
|
1077
|
-
// If no focused node and no implementing nodes, show
|
|
1136
|
+
// If no focused node and no implementing nodes, show actionable nodes only.
|
|
1137
|
+
// Filter out decided/implemented/resolved — those are done; the footer should
|
|
1138
|
+
// surface work in progress, not a historical catalog.
|
|
1078
1139
|
if (!dt.focusedNode && (!dt.implementingNodes || dt.implementingNodes.length === 0) && dt.nodes) {
|
|
1140
|
+
const TERMINAL_STATUSES = new Set(["decided", "implemented", "resolved"]);
|
|
1141
|
+
const actionable = dt.nodes.filter(n => !TERMINAL_STATUSES.has(n.status));
|
|
1079
1142
|
const MAX_NODES = 6;
|
|
1080
|
-
for (const n of
|
|
1143
|
+
for (const n of actionable.slice(0, MAX_NODES)) {
|
|
1081
1144
|
const icon = this.nodeStatusIcon(n.status);
|
|
1082
1145
|
const badge = designSpecBadge(n.designSpec, n.assessmentResult, (c, t) => theme.fg(c as any, t));
|
|
1083
1146
|
const badgeSep = badge ? " " : "";
|
|
@@ -1090,9 +1153,9 @@ export class DashboardFooter implements Component {
|
|
|
1090
1153
|
[qSuffix + linkSuffix],
|
|
1091
1154
|
));
|
|
1092
1155
|
}
|
|
1093
|
-
if (
|
|
1156
|
+
if (actionable.length > MAX_NODES) {
|
|
1094
1157
|
lines.push(
|
|
1095
|
-
theme.fg("dim", ` … ${
|
|
1158
|
+
theme.fg("dim", ` … ${actionable.length - MAX_NODES} more`) +
|
|
1096
1159
|
theme.fg("dim", " — /dashboard to expand"),
|
|
1097
1160
|
);
|
|
1098
1161
|
}
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
validateNodeId,
|
|
60
60
|
scaffoldOpenSpecChange,
|
|
61
61
|
scaffoldDesignOpenSpecChange,
|
|
62
|
+
extractAndArchiveDesignSpec,
|
|
62
63
|
mirrorOpenQuestionsToDesignSpec,
|
|
63
64
|
matchBranchToNode,
|
|
64
65
|
appendBranch,
|
|
@@ -798,31 +799,44 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
798
799
|
}
|
|
799
800
|
const oldStatus = node.status;
|
|
800
801
|
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
802
|
+
// Gate: set_status(decided) checks substance first, then artifacts.
|
|
803
|
+
// Substance = open questions resolved, decisions recorded.
|
|
804
|
+
// Artifacts = design spec archived (auto-created if missing).
|
|
804
805
|
if (newStatus === "decided") {
|
|
806
|
+
const openQs = node.open_questions?.length ?? 0;
|
|
807
|
+
|
|
808
|
+
// Substance check: open questions must be resolved (fast — no file I/O)
|
|
809
|
+
if (openQs > 0) {
|
|
810
|
+
return {
|
|
811
|
+
content: [{ type: "text", text:
|
|
812
|
+
`⚠ Cannot decide '${node.title}': ${openQs} open question${openQs > 1 ? "s" : ""} remain\n` +
|
|
813
|
+
`→ Resolve with add_decision/remove_question, or branch child nodes for exploration` }],
|
|
814
|
+
details: { id: node.id, blockedBy: "open-questions", openQuestions: openQs },
|
|
815
|
+
isError: true,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Substance check: at least one decision recorded (skip for lightweight types)
|
|
820
|
+
// Parse doc only if we need to check decisions (past the open-questions gate).
|
|
821
|
+
const sections = getNodeSections(node);
|
|
805
822
|
const lightweightTypes = new Set(["bug", "chore", "task"]);
|
|
806
823
|
const isLightweight = node.issue_type && lightweightTypes.has(node.issue_type);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
details: { id: node.id, blockedBy: "design-openspec-not-archived" },
|
|
824
|
-
isError: true,
|
|
825
|
-
};
|
|
824
|
+
if (!isLightweight && sections.decisions.length === 0) {
|
|
825
|
+
return {
|
|
826
|
+
content: [{ type: "text", text:
|
|
827
|
+
`⚠ Cannot decide '${node.title}': no decisions recorded\n` +
|
|
828
|
+
`→ Use add_decision to record at least one design decision before marking decided` }],
|
|
829
|
+
details: { id: node.id, blockedBy: "no-decisions" },
|
|
830
|
+
isError: true,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Artifact check: auto-scaffold + auto-archive if missing
|
|
835
|
+
const designSpec = resolveDesignSpecBinding(ctx.cwd, node.id);
|
|
836
|
+
if (!designSpec.archived) {
|
|
837
|
+
const extracted = extractAndArchiveDesignSpec(ctx.cwd, node);
|
|
838
|
+
if (extracted.created) {
|
|
839
|
+
// Auto-scaffolded — proceed (message appended below)
|
|
826
840
|
}
|
|
827
841
|
}
|
|
828
842
|
}
|
|
@@ -945,10 +959,19 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
945
959
|
if (!node) {
|
|
946
960
|
return { content: [{ type: "text", text: `Node '${params.node_id}' not found` }], details: {}, isError: true };
|
|
947
961
|
}
|
|
948
|
-
|
|
962
|
+
// Auto-transition seed → exploring on first substance addition
|
|
963
|
+
let transitioned = "";
|
|
964
|
+
let currentNode = node;
|
|
965
|
+
if (node.status === "seed") {
|
|
966
|
+
currentNode = setNodeStatus(node, "exploring");
|
|
967
|
+
tree.nodes.set(currentNode.id, currentNode);
|
|
968
|
+
scaffoldDesignOpenSpecChange(ctx.cwd, currentNode);
|
|
969
|
+
transitioned = " (auto-transitioned seed → exploring)";
|
|
970
|
+
}
|
|
971
|
+
addResearch(currentNode, params.heading, params.content);
|
|
949
972
|
emitCurrentState();
|
|
950
973
|
return {
|
|
951
|
-
content: [{ type: "text", text: `Added research '${params.heading}' to '${node.title}'` }],
|
|
974
|
+
content: [{ type: "text", text: `Added research '${params.heading}' to '${node.title}'${transitioned}` }],
|
|
952
975
|
details: { id: node.id, heading: params.heading },
|
|
953
976
|
};
|
|
954
977
|
}
|
|
@@ -962,13 +985,20 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
962
985
|
if (!node) {
|
|
963
986
|
return { content: [{ type: "text", text: `Node '${params.node_id}' not found` }], details: {}, isError: true };
|
|
964
987
|
}
|
|
988
|
+
// Auto-transition seed → exploring on first substance addition
|
|
989
|
+
let currentNode = node;
|
|
990
|
+
if (node.status === "seed") {
|
|
991
|
+
currentNode = setNodeStatus(node, "exploring");
|
|
992
|
+
tree.nodes.set(currentNode.id, currentNode);
|
|
993
|
+
scaffoldDesignOpenSpecChange(ctx.cwd, currentNode);
|
|
994
|
+
}
|
|
965
995
|
const validDecisionStatuses = ["exploring", "decided", "rejected"];
|
|
966
996
|
const rawDStatus = params.decision_status || "exploring";
|
|
967
997
|
if (!validDecisionStatuses.includes(rawDStatus)) {
|
|
968
998
|
return { content: [{ type: "text", text: `Invalid decision_status '${rawDStatus}'. Valid: ${validDecisionStatuses.join(", ")}` }], details: {}, isError: true };
|
|
969
999
|
}
|
|
970
1000
|
const dStatus = rawDStatus as "exploring" | "decided" | "rejected";
|
|
971
|
-
addDecision(
|
|
1001
|
+
addDecision(currentNode, {
|
|
972
1002
|
title: params.decision_title,
|
|
973
1003
|
status: dStatus,
|
|
974
1004
|
rationale: params.rationale || "",
|
|
@@ -1136,43 +1166,25 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1136
1166
|
return { content: [{ type: "text", text: `Node '${params.node_id}' not found` }], details: {}, isError: true };
|
|
1137
1167
|
}
|
|
1138
1168
|
if (node.status !== "decided" && node.status !== "resolved") {
|
|
1169
|
+
const openQs = node.open_questions?.length ?? 0;
|
|
1170
|
+
const hint = openQs > 0
|
|
1171
|
+
? `→ Resolve ${openQs} open question${openQs > 1 ? "s" : ""}, then run set_status(decided)`
|
|
1172
|
+
: `→ Run set_status(decided) to advance this node`;
|
|
1139
1173
|
return {
|
|
1140
|
-
content: [{
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
`Resolve open questions and set status to 'decided' (or 'resolved') before implementing.`,
|
|
1144
|
-
}],
|
|
1145
|
-
details: {},
|
|
1174
|
+
content: [{ type: "text", text:
|
|
1175
|
+
`⚠ Cannot implement '${node.title}': status is '${node.status}', needs 'decided' or 'resolved'\n${hint}` }],
|
|
1176
|
+
details: { id: node.id, blockedBy: "status", currentStatus: node.status },
|
|
1146
1177
|
isError: true,
|
|
1147
1178
|
};
|
|
1148
1179
|
}
|
|
1149
1180
|
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
// spec because the decision is the bug diagnosis itself.
|
|
1181
|
+
// Design-spec gate: if not yet archived, auto-extract from doc.
|
|
1182
|
+
// The decided gate already handles substance checks — implement
|
|
1183
|
+
// only needs the artifact to exist for audit trail.
|
|
1154
1184
|
{
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const skipDesignGate = isLightweight && !hasOpenQuestions && (node.status === "decided" || node.status === "resolved");
|
|
1159
|
-
|
|
1160
|
-
if (!skipDesignGate) {
|
|
1161
|
-
const designSpec = resolveDesignSpecBinding(ctx.cwd, node.id);
|
|
1162
|
-
if (designSpec.missing) {
|
|
1163
|
-
return {
|
|
1164
|
-
content: [{ type: "text", text: "Scaffold design spec first via set_status(exploring)" }],
|
|
1165
|
-
details: { id: node.id, blockedBy: "design-openspec-missing" },
|
|
1166
|
-
isError: true,
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
if (designSpec.active && !designSpec.archived) {
|
|
1170
|
-
return {
|
|
1171
|
-
content: [{ type: "text", text: `Cannot implement '${node.title}': archive the design change first (\`/opsx:archive\` on the design change).` }],
|
|
1172
|
-
details: { id: node.id, blockedBy: "design-openspec-not-archived" },
|
|
1173
|
-
isError: true,
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1185
|
+
const designSpec = resolveDesignSpecBinding(ctx.cwd, node.id);
|
|
1186
|
+
if (!designSpec.archived) {
|
|
1187
|
+
extractAndArchiveDesignSpec(ctx.cwd, node);
|
|
1176
1188
|
}
|
|
1177
1189
|
}
|
|
1178
1190
|
|
|
@@ -1180,6 +1192,25 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1180
1192
|
reload(ctx.cwd);
|
|
1181
1193
|
emitCurrentState();
|
|
1182
1194
|
|
|
1195
|
+
// Auto-focus the design node and fork a directive-scoped memory mind
|
|
1196
|
+
// so context injection and fact isolation track the active directive.
|
|
1197
|
+
if (implResult.ok) {
|
|
1198
|
+
// Auto-focus so context injection immediately tracks this node
|
|
1199
|
+
focusedNode = node.id;
|
|
1200
|
+
|
|
1201
|
+
// Publish active directive for session-start and dashboard
|
|
1202
|
+
sharedState.activeDirective = {
|
|
1203
|
+
nodeId: node.id,
|
|
1204
|
+
branch: implResult.branch ?? `feature/${node.id}`,
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const mindName = `directive/${node.id}`;
|
|
1208
|
+
(sharedState.mindLifecycleQueue ??= []).push(
|
|
1209
|
+
{ action: "fork", mind: mindName, description: `Memory scope for ${implResult.branch ?? node.id}` },
|
|
1210
|
+
{ action: "activate", mind: mindName },
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1183
1214
|
return {
|
|
1184
1215
|
content: [{ type: "text", text: implResult.message }],
|
|
1185
1216
|
details: implResult.ok
|
|
@@ -1605,3 +1605,116 @@ export function mirrorOpenQuestionsToDesignSpec(cwd: string, node: DesignNode):
|
|
|
1605
1605
|
const content = buildDesignTasksContent(node, syntheticSections as DocumentSections);
|
|
1606
1606
|
fs.writeFileSync(tasksPath, content);
|
|
1607
1607
|
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Extract a design-spec artifact from the doc's structured content.
|
|
1611
|
+
* Deterministic: no LLM pass, no placeholders. If the doc has thin content,
|
|
1612
|
+
* the extracted spec reflects that honestly.
|
|
1613
|
+
*
|
|
1614
|
+
* Creates openspec/design/{id}/ with proposal.md, spec.md, and archives it
|
|
1615
|
+
* immediately to openspec/design-archive/{date}-{id}/.
|
|
1616
|
+
*
|
|
1617
|
+
* Returns { created, archived, message }.
|
|
1618
|
+
*/
|
|
1619
|
+
export function extractAndArchiveDesignSpec(
|
|
1620
|
+
cwd: string,
|
|
1621
|
+
node: DesignNode,
|
|
1622
|
+
): { created: boolean; archived: boolean; message: string } {
|
|
1623
|
+
const designDir = path.join(cwd, "openspec", "design", node.id);
|
|
1624
|
+
const archiveBaseDir = path.join(cwd, "openspec", "design-archive");
|
|
1625
|
+
|
|
1626
|
+
// Already archived? Nothing to do.
|
|
1627
|
+
if (fs.existsSync(archiveBaseDir)) {
|
|
1628
|
+
for (const entry of fs.readdirSync(archiveBaseDir, { withFileTypes: true })) {
|
|
1629
|
+
if (!entry.isDirectory()) continue;
|
|
1630
|
+
const match = entry.name.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
1631
|
+
if (match && match[1] === node.id) {
|
|
1632
|
+
return { created: false, archived: true, message: "Design spec already archived" };
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
let sections: DocumentSections;
|
|
1638
|
+
try {
|
|
1639
|
+
sections = getNodeSections(node);
|
|
1640
|
+
} catch {
|
|
1641
|
+
sections = {
|
|
1642
|
+
overview: "",
|
|
1643
|
+
research: [],
|
|
1644
|
+
decisions: [],
|
|
1645
|
+
openQuestions: node.open_questions ?? [],
|
|
1646
|
+
implementationNotes: { fileScope: [], constraints: [], rawContent: "" },
|
|
1647
|
+
acceptanceCriteria: { scenarios: [], falsifiability: [], constraints: [] },
|
|
1648
|
+
extraSections: [],
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// ── Build spec content from doc sections ──────────────────────
|
|
1653
|
+
const specLines: string[] = [
|
|
1654
|
+
`# ${node.title} — Design Spec (extracted)`,
|
|
1655
|
+
"",
|
|
1656
|
+
`> Auto-extracted from docs/${node.id}.md at decide-time.`,
|
|
1657
|
+
"",
|
|
1658
|
+
];
|
|
1659
|
+
|
|
1660
|
+
if (sections.decisions.length > 0) {
|
|
1661
|
+
specLines.push("## Decisions", "");
|
|
1662
|
+
for (const d of sections.decisions) {
|
|
1663
|
+
specLines.push(`### ${d.title} (${d.status})`);
|
|
1664
|
+
if (d.rationale) specLines.push("", d.rationale);
|
|
1665
|
+
specLines.push("");
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const ac = sections.acceptanceCriteria;
|
|
1670
|
+
if (ac.scenarios.length > 0 || ac.falsifiability.length > 0 || ac.constraints.length > 0) {
|
|
1671
|
+
specLines.push("## Acceptance Criteria", "");
|
|
1672
|
+
if (ac.scenarios.length > 0) {
|
|
1673
|
+
specLines.push("### Scenarios", "");
|
|
1674
|
+
for (const s of ac.scenarios) specLines.push(`- ${s}`);
|
|
1675
|
+
specLines.push("");
|
|
1676
|
+
}
|
|
1677
|
+
if (ac.falsifiability.length > 0) {
|
|
1678
|
+
specLines.push("### Falsifiability", "");
|
|
1679
|
+
for (const f of ac.falsifiability) specLines.push(`- ${f}`);
|
|
1680
|
+
specLines.push("");
|
|
1681
|
+
}
|
|
1682
|
+
if (ac.constraints.length > 0) {
|
|
1683
|
+
specLines.push("### Constraints", "");
|
|
1684
|
+
for (const c of ac.constraints) specLines.push(`- ${c}`);
|
|
1685
|
+
specLines.push("");
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (sections.research.length > 0) {
|
|
1690
|
+
specLines.push("## Research Summary", "");
|
|
1691
|
+
for (const r of sections.research) {
|
|
1692
|
+
specLines.push(`### ${r.heading}`, "", r.content.slice(0, 500) + (r.content.length > 500 ? "…" : ""), "");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// ── Write to openspec/design/{id}/ ──────────────────────────
|
|
1697
|
+
// Preserve any existing files (e.g. tasks.md from scaffold), overwrite proposal/spec.
|
|
1698
|
+
fs.mkdirSync(designDir, { recursive: true });
|
|
1699
|
+
fs.writeFileSync(
|
|
1700
|
+
path.join(designDir, "proposal.md"),
|
|
1701
|
+
`# ${node.title}\n\n## Intent\n\n${sections.overview?.split("\n").find(l => l.trim()) ?? node.title}\n\nSee [design doc](../../../docs/${node.id}.md).\n`,
|
|
1702
|
+
);
|
|
1703
|
+
fs.writeFileSync(path.join(designDir, "spec.md"), specLines.join("\n"));
|
|
1704
|
+
|
|
1705
|
+
// ── Immediately archive (copy ALL files, not just ours) ──────
|
|
1706
|
+
const today = new Date().toISOString().split("T")[0];
|
|
1707
|
+
const archiveDir = path.join(archiveBaseDir, `${today}-${node.id}`);
|
|
1708
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
1709
|
+
|
|
1710
|
+
for (const file of fs.readdirSync(designDir)) {
|
|
1711
|
+
fs.copyFileSync(path.join(designDir, file), path.join(archiveDir, file));
|
|
1712
|
+
}
|
|
1713
|
+
fs.rmSync(designDir, { recursive: true, force: true });
|
|
1714
|
+
|
|
1715
|
+
return {
|
|
1716
|
+
created: true,
|
|
1717
|
+
archived: true,
|
|
1718
|
+
message: `Extracted design spec from docs/${node.id}.md (${sections.decisions.length} decisions, ${sections.research.length} research sections) → archived to openspec/design-archive/${today}-${node.id}/`,
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
@@ -135,6 +135,30 @@ interface SharedState {
|
|
|
135
135
|
/** Set by bootstrap when first-run is detected. Other extensions should suppress
|
|
136
136
|
* redundant "no providers" warnings when this is true — bootstrap handles guidance. */
|
|
137
137
|
bootstrapPending?: boolean;
|
|
138
|
+
|
|
139
|
+
/** Pending mind lifecycle operations from design-tree/openspec for project-memory to process.
|
|
140
|
+
* Written by implement/archive flows, consumed by project-memory on next turn. */
|
|
141
|
+
mindLifecycleQueue?: MindLifecycleRequest[];
|
|
142
|
+
|
|
143
|
+
/** Active directive: the design node currently being implemented.
|
|
144
|
+
* Set by design_tree_update(implement), read by session-start and dashboard. */
|
|
145
|
+
activeDirective?: { nodeId: string; branch: string } | null;
|
|
146
|
+
|
|
147
|
+
/** Active mind name from factstore. Written by project-memory, read by dashboard.
|
|
148
|
+
* null means default mind (no directive active). */
|
|
149
|
+
activeMind?: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type MindLifecycleAction = "fork" | "activate" | "ingest" | "delete";
|
|
153
|
+
|
|
154
|
+
export interface MindLifecycleRequest {
|
|
155
|
+
action: MindLifecycleAction;
|
|
156
|
+
/** Mind name: fork target, activate target, ingest source, or delete target. */
|
|
157
|
+
mind: string;
|
|
158
|
+
/** Human-readable description (used by fork). */
|
|
159
|
+
description?: string;
|
|
160
|
+
/** Target mind for ingest (defaults to 'default'). */
|
|
161
|
+
targetMind?: string;
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
// Initialize once on first import, reuse thereafter via global symbol.
|