@kontextmind/kxm 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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KXM Web Studio Layout Engine (Phase 10 / Decision D14)
|
|
3
|
+
* Generates form/stepper metadata, ELK/React Flow-derived DAG nodes/edges,
|
|
4
|
+
* and Temporal swimlane activity timelines from compiled plans and run states.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VnextCompiledPlan, VnextCompiledStep } from "./vnext-engine-compile.ts";
|
|
8
|
+
import type { VnextRunState } from "./vnext-engine-fold.ts";
|
|
9
|
+
|
|
10
|
+
export const STUDIO_LAYOUT_SCHEMA = "kxm.studio-layout.v1" as const;
|
|
11
|
+
|
|
12
|
+
export interface StudioDagNode {
|
|
13
|
+
id: string;
|
|
14
|
+
type: "agentStep" | "gateStep" | "approvalStep" | "waitStep";
|
|
15
|
+
data: {
|
|
16
|
+
label: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
role?: string | undefined;
|
|
19
|
+
gate?: string | undefined;
|
|
20
|
+
status: "pending" | "preparing" | "running" | "passed" | "failed" | "waiting" | "cancelled";
|
|
21
|
+
stepAttempt?: number | undefined;
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
outcomes: readonly string[];
|
|
24
|
+
};
|
|
25
|
+
position: { x: number; y: number };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StudioDagEdge {
|
|
29
|
+
id: string;
|
|
30
|
+
source: string;
|
|
31
|
+
target: string;
|
|
32
|
+
label?: string | undefined;
|
|
33
|
+
animated?: boolean | undefined;
|
|
34
|
+
style?: { stroke?: string } | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TemporalSwimlaneActivity {
|
|
38
|
+
stepId: string;
|
|
39
|
+
attemptId?: string | undefined;
|
|
40
|
+
status: string;
|
|
41
|
+
startOffsetMs: number;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
stepAttempt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TemporalSwimlane {
|
|
47
|
+
role: string;
|
|
48
|
+
agentId?: string | undefined;
|
|
49
|
+
activities: TemporalSwimlaneActivity[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface StudioStepperStage {
|
|
53
|
+
id: string;
|
|
54
|
+
label: string;
|
|
55
|
+
kind: string;
|
|
56
|
+
status: "pending" | "active" | "completed" | "failed" | "skipped";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface StudioLayoutPayload {
|
|
60
|
+
schema: typeof STUDIO_LAYOUT_SCHEMA;
|
|
61
|
+
workflowId: string;
|
|
62
|
+
runId?: string | undefined;
|
|
63
|
+
generatedAt: string;
|
|
64
|
+
stepper: StudioStepperStage[];
|
|
65
|
+
dag: {
|
|
66
|
+
nodes: StudioDagNode[];
|
|
67
|
+
edges: StudioDagEdge[];
|
|
68
|
+
};
|
|
69
|
+
temporalSwimlanes: TemporalSwimlane[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Computes deterministic layered DAG coordinates (ELK-style hierarchical ranking)
|
|
74
|
+
* without requiring any manual layout coordinates in the YAML workflow file.
|
|
75
|
+
*/
|
|
76
|
+
export function generateStudioLayout(
|
|
77
|
+
plan: VnextCompiledPlan,
|
|
78
|
+
state?: VnextRunState | undefined,
|
|
79
|
+
): StudioLayoutPayload {
|
|
80
|
+
const generatedAt = new Date().toISOString();
|
|
81
|
+
const stepIds = Object.keys(plan.steps);
|
|
82
|
+
|
|
83
|
+
// 1. Calculate topological rank/level for each step
|
|
84
|
+
const inDegree: Record<string, number> = {};
|
|
85
|
+
const adj: Record<string, string[]> = {};
|
|
86
|
+
for (const id of stepIds) {
|
|
87
|
+
inDegree[id] = 0;
|
|
88
|
+
adj[id] = [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [sourceId, step] of Object.entries(plan.steps)) {
|
|
92
|
+
for (const transition of Object.values(step.transitions)) {
|
|
93
|
+
if (transition.to && plan.steps[transition.to]) {
|
|
94
|
+
adj[sourceId]!.push(transition.to);
|
|
95
|
+
inDegree[transition.to] = (inDegree[transition.to] ?? 0) + 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Assign levels (x coordinate columns)
|
|
101
|
+
const levels: Record<string, number> = {};
|
|
102
|
+
const queue: string[] = [];
|
|
103
|
+
|
|
104
|
+
if (plan.entryStepId && plan.steps[plan.entryStepId]) {
|
|
105
|
+
queue.push(plan.entryStepId);
|
|
106
|
+
levels[plan.entryStepId] = 0;
|
|
107
|
+
} else {
|
|
108
|
+
for (const id of stepIds) {
|
|
109
|
+
if ((inDegree[id] ?? 0) === 0) {
|
|
110
|
+
queue.push(id);
|
|
111
|
+
levels[id] = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
while (queue.length > 0) {
|
|
117
|
+
const current = queue.shift()!;
|
|
118
|
+
const curLevel = levels[current] ?? 0;
|
|
119
|
+
for (const neighbor of adj[current] ?? []) {
|
|
120
|
+
const nextLevel = Math.max(levels[neighbor] ?? 0, curLevel + 1);
|
|
121
|
+
levels[neighbor] = nextLevel;
|
|
122
|
+
queue.push(neighbor);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Default any unassigned step to 0
|
|
127
|
+
for (const id of stepIds) {
|
|
128
|
+
if (levels[id] === undefined) levels[id] = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Group steps by level to determine y positions
|
|
132
|
+
const byLevel: Record<number, string[]> = {};
|
|
133
|
+
for (const [id, lvl] of Object.entries(levels)) {
|
|
134
|
+
if (!byLevel[lvl]) byLevel[lvl] = [];
|
|
135
|
+
byLevel[lvl]!.push(id);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const nodes: StudioDagNode[] = [];
|
|
139
|
+
const edges: StudioDagEdge[] = [];
|
|
140
|
+
const X_SPACING = 240;
|
|
141
|
+
const Y_SPACING = 120;
|
|
142
|
+
|
|
143
|
+
for (const [lvlStr, idsInLevel] of Object.entries(byLevel)) {
|
|
144
|
+
const lvl = Number(lvlStr);
|
|
145
|
+
const x = lvl * X_SPACING;
|
|
146
|
+
const startY = -(idsInLevel.length - 1) * (Y_SPACING / 2);
|
|
147
|
+
|
|
148
|
+
idsInLevel.forEach((stepId, index) => {
|
|
149
|
+
const step = plan.steps[stepId]!;
|
|
150
|
+
const y = startY + index * Y_SPACING;
|
|
151
|
+
|
|
152
|
+
let status: StudioDagNode["data"]["status"] = "pending";
|
|
153
|
+
let stepAttempt: number | undefined = undefined;
|
|
154
|
+
|
|
155
|
+
if (state) {
|
|
156
|
+
if (state.currentStep?.stepId === stepId) {
|
|
157
|
+
status = state.currentStep.status === "running" ? "running" : "preparing";
|
|
158
|
+
stepAttempt = state.currentStep.stepAttempt;
|
|
159
|
+
} else if (state.stepAttempts[stepId] !== undefined) {
|
|
160
|
+
status = "passed";
|
|
161
|
+
stepAttempt = state.stepAttempts[stepId];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let type: StudioDagNode["type"] = "agentStep";
|
|
166
|
+
let role: string | undefined = undefined;
|
|
167
|
+
let gate: string | undefined = undefined;
|
|
168
|
+
|
|
169
|
+
if (step.kind === "agent" || step.kind === "moa") {
|
|
170
|
+
type = "agentStep";
|
|
171
|
+
role = step.agent;
|
|
172
|
+
} else if (step.kind === "gate") {
|
|
173
|
+
type = "gateStep";
|
|
174
|
+
gate = step.gate;
|
|
175
|
+
} else if (step.kind === "approval") {
|
|
176
|
+
type = "approvalStep";
|
|
177
|
+
} else if (step.kind === "wait") {
|
|
178
|
+
type = "waitStep";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
nodes.push({
|
|
182
|
+
id: stepId,
|
|
183
|
+
type,
|
|
184
|
+
data: {
|
|
185
|
+
label: step.description ?? stepId,
|
|
186
|
+
kind: step.kind,
|
|
187
|
+
role,
|
|
188
|
+
gate,
|
|
189
|
+
status,
|
|
190
|
+
stepAttempt,
|
|
191
|
+
description: step.instructions,
|
|
192
|
+
outcomes: step.outcomes,
|
|
193
|
+
},
|
|
194
|
+
position: { x, y },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Build edges
|
|
198
|
+
for (const [outcome, transition] of Object.entries(step.transitions)) {
|
|
199
|
+
if (transition.to && plan.steps[transition.to]) {
|
|
200
|
+
edges.push({
|
|
201
|
+
id: `e_${stepId}_to_${transition.to}_${outcome}`,
|
|
202
|
+
source: stepId,
|
|
203
|
+
target: transition.to,
|
|
204
|
+
label: outcome !== "passed" && outcome !== "completed" ? outcome : undefined,
|
|
205
|
+
animated: status === "running",
|
|
206
|
+
style: {
|
|
207
|
+
stroke: outcome === "failed" ? "#ef4444" : outcome === "warning" ? "#f59e0b" : "#64748b",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 2. Stepper stages (linear path)
|
|
216
|
+
const stepper: StudioStepperStage[] = plan.order.map((stepId) => {
|
|
217
|
+
const step = plan.steps[stepId];
|
|
218
|
+
let status: StudioStepperStage["status"] = "pending";
|
|
219
|
+
if (state) {
|
|
220
|
+
if (state.currentStep?.stepId === stepId) {
|
|
221
|
+
status = "active";
|
|
222
|
+
} else if (state.stepAttempts[stepId] !== undefined) {
|
|
223
|
+
status = "completed";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
id: stepId,
|
|
228
|
+
label: step?.description ?? stepId,
|
|
229
|
+
kind: step?.kind ?? "unknown",
|
|
230
|
+
status,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 3. Temporal Swimlanes
|
|
235
|
+
const swimlanesByRole: Record<string, TemporalSwimlaneActivity[]> = {};
|
|
236
|
+
let currentOffset = 0;
|
|
237
|
+
|
|
238
|
+
for (const stepId of plan.order) {
|
|
239
|
+
const step = plan.steps[stepId];
|
|
240
|
+
const role = step && (step.kind === "agent" || step.kind === "moa") ? step.agent : "engine-coordinator";
|
|
241
|
+
if (!swimlanesByRole[role]) swimlanesByRole[role] = [];
|
|
242
|
+
|
|
243
|
+
const duration = step?.timeoutMs ? Math.min(step.timeoutMs, 5000) : 3000;
|
|
244
|
+
swimlanesByRole[role]!.push({
|
|
245
|
+
stepId,
|
|
246
|
+
status: state?.currentStep?.stepId === stepId ? "running" : "completed",
|
|
247
|
+
startOffsetMs: currentOffset,
|
|
248
|
+
durationMs: duration,
|
|
249
|
+
stepAttempt: state?.stepAttempts[stepId] ?? 1,
|
|
250
|
+
});
|
|
251
|
+
currentOffset += duration;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const temporalSwimlanes: TemporalSwimlane[] = Object.entries(swimlanesByRole).map(([role, activities]) => ({
|
|
255
|
+
role,
|
|
256
|
+
activities,
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
schema: STUDIO_LAYOUT_SCHEMA,
|
|
261
|
+
workflowId: plan.workflowId,
|
|
262
|
+
runId: state ? "active-run" : undefined,
|
|
263
|
+
generatedAt,
|
|
264
|
+
stepper,
|
|
265
|
+
dag: { nodes, edges },
|
|
266
|
+
temporalSwimlanes,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export interface RoleSpec {
|
|
2
|
+
harness: string;
|
|
3
|
+
model: string;
|
|
4
|
+
role: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SuggestionResult {
|
|
8
|
+
workflowId: string;
|
|
9
|
+
area: string;
|
|
10
|
+
confidence: number;
|
|
11
|
+
reasons: string[];
|
|
12
|
+
suggestedSkills: string[];
|
|
13
|
+
roles: {
|
|
14
|
+
planner: RoleSpec;
|
|
15
|
+
writer: RoleSpec;
|
|
16
|
+
critics: RoleSpec[];
|
|
17
|
+
verifier: { kind: "witness"; command: string };
|
|
18
|
+
};
|
|
19
|
+
suggestedCommand: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WorkflowPattern {
|
|
23
|
+
id: string;
|
|
24
|
+
area: string;
|
|
25
|
+
keywords: string[];
|
|
26
|
+
skills: string[];
|
|
27
|
+
defaultCommand: (prompt: string) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const WORKFLOW_PATTERNS: WorkflowPattern[] = [
|
|
31
|
+
{
|
|
32
|
+
id: "software-engineering/bug-fix",
|
|
33
|
+
area: "software-engineering",
|
|
34
|
+
keywords: ["fix", "bug", "flaky", "failure", "timeout", "error", "repro", "crash", "broken", "hang"],
|
|
35
|
+
skills: ["troubleshooting", "memory-leak-debugging"],
|
|
36
|
+
defaultCommand: (p) => `kxm run software-engineering/bug-fix "${p}"`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "software-engineering/feature-implementation",
|
|
40
|
+
area: "software-engineering",
|
|
41
|
+
keywords: ["feature", "implement", "add", "build", "create", "develop", "support", "endpoint", "ui", "tui"],
|
|
42
|
+
skills: ["modern-web-guidance", "kxm"],
|
|
43
|
+
defaultCommand: (p) => `kxm run software-engineering/feature-implementation "${p}"`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "software-engineering/refactoring",
|
|
47
|
+
area: "software-engineering",
|
|
48
|
+
keywords: ["refactor", "cleanup", "reorganize", "modularize", "deduplicate", "split", "simplify", "deprecate"],
|
|
49
|
+
skills: ["kxm"],
|
|
50
|
+
defaultCommand: (p) => `kxm run software-engineering/refactoring "${p}"`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "security-reliability/vulnerability-remediation",
|
|
54
|
+
area: "security-reliability",
|
|
55
|
+
keywords: ["cve", "vulnerability", "security", "exploit", "sanitize", "leak", "secret", "injection", "redact", "auth"],
|
|
56
|
+
skills: ["kxm"],
|
|
57
|
+
defaultCommand: (p) => `kxm run security-reliability/vulnerability-remediation "${p}"`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "security-reliability/reliability-hardening",
|
|
61
|
+
area: "security-reliability",
|
|
62
|
+
keywords: ["idempotency", "retry", "circuit-breaker", "cas", "lock", "concurrency", "deadlock", "race", "crash-recovery"],
|
|
63
|
+
skills: ["kxm"],
|
|
64
|
+
defaultCommand: (p) => `kxm run security-reliability/reliability-hardening "${p}"`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "data-analytics/pipeline-migration",
|
|
68
|
+
area: "data-analytics",
|
|
69
|
+
keywords: ["database", "sqlite", "migration", "pipeline", "schema", "transform", "table", "wal", "foreign", "cascading"],
|
|
70
|
+
skills: ["kxm"],
|
|
71
|
+
defaultCommand: (p) => `kxm run data-analytics/pipeline-migration "${p}"`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "research-strategy/architecture-spike",
|
|
75
|
+
area: "research-strategy",
|
|
76
|
+
keywords: ["spike", "investigate", "prototype", "research", "feasibility", "benchmark", "explore", "evaluate"],
|
|
77
|
+
skills: ["kxm-session"],
|
|
78
|
+
defaultCommand: (p) => `kxm run research-strategy/architecture-spike "${p}"`,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export function suggestWorkflowAndRoles(
|
|
83
|
+
prompt: string,
|
|
84
|
+
options: {
|
|
85
|
+
availableHarnesses?: Array<{ harness: string; auth?: string | undefined }> | undefined;
|
|
86
|
+
} = {},
|
|
87
|
+
): SuggestionResult {
|
|
88
|
+
const normalized = prompt.toLowerCase();
|
|
89
|
+
const words = normalized.split(/\W+/).filter(Boolean);
|
|
90
|
+
|
|
91
|
+
let bestPattern: WorkflowPattern = WORKFLOW_PATTERNS[0]!;
|
|
92
|
+
let bestScore = -1;
|
|
93
|
+
const matchedReasons: string[] = [];
|
|
94
|
+
|
|
95
|
+
for (const pattern of WORKFLOW_PATTERNS) {
|
|
96
|
+
let score = 0;
|
|
97
|
+
const matchedWords: string[] = [];
|
|
98
|
+
for (const keyword of pattern.keywords) {
|
|
99
|
+
if (words.includes(keyword) || normalized.includes(keyword)) {
|
|
100
|
+
score += 1;
|
|
101
|
+
matchedWords.push(keyword);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (score > bestScore) {
|
|
105
|
+
bestScore = score;
|
|
106
|
+
bestPattern = pattern;
|
|
107
|
+
matchedReasons.length = 0;
|
|
108
|
+
matchedReasons.push(`Matched keywords: ${matchedWords.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (bestScore <= 0) {
|
|
113
|
+
// Default fallback to feature-implementation
|
|
114
|
+
bestPattern = WORKFLOW_PATTERNS[1]!;
|
|
115
|
+
matchedReasons.push("Default fallback: general feature implementation workflow");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Determine active harnesses
|
|
119
|
+
const activeHarnesses = new Set(
|
|
120
|
+
(options.availableHarnesses ?? [])
|
|
121
|
+
.filter((h) => h.auth === "active" || h.auth === "ready" || h.auth === "configured")
|
|
122
|
+
.map((h) => h.harness.toLowerCase()),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// If none specified or active, assume standard default rotation
|
|
126
|
+
const hasClaude = activeHarnesses.size === 0 || activeHarnesses.has("claude");
|
|
127
|
+
const hasGrok = activeHarnesses.size === 0 || activeHarnesses.has("grok");
|
|
128
|
+
const hasCodex = activeHarnesses.size === 0 || activeHarnesses.has("codex");
|
|
129
|
+
|
|
130
|
+
// Planner
|
|
131
|
+
const planner: RoleSpec = hasClaude
|
|
132
|
+
? { harness: "claude", model: "fable", role: "planner" }
|
|
133
|
+
: { harness: "codex", model: "gpt-5.6-sol", role: "planner" };
|
|
134
|
+
|
|
135
|
+
// Writer (Starting rotation: Grok 4.6 headless)
|
|
136
|
+
const writer: RoleSpec = hasGrok
|
|
137
|
+
? { harness: "grok", model: "grok-4.6", role: "writer" }
|
|
138
|
+
: (activeHarnesses.has("agy")
|
|
139
|
+
? { harness: "agy", model: "gemini-2.5-pro", role: "writer" }
|
|
140
|
+
: { harness: "pi", model: "qwen/qwen3-coder-plus", role: "writer" });
|
|
141
|
+
|
|
142
|
+
// Critics (Vendor independent: Fable Arch + Codex CLI)
|
|
143
|
+
const critics: RoleSpec[] = [
|
|
144
|
+
{ harness: "claude", model: "fable", role: "reviewer-arch" },
|
|
145
|
+
{ harness: "codex", model: "gpt-5.6-sol", role: "reviewer-cli" },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
workflowId: bestPattern.id,
|
|
150
|
+
area: bestPattern.area,
|
|
151
|
+
confidence: bestScore > 0 ? Math.min(1, 0.5 + bestScore * 0.15) : 0.5,
|
|
152
|
+
reasons: matchedReasons,
|
|
153
|
+
suggestedSkills: bestPattern.skills,
|
|
154
|
+
roles: {
|
|
155
|
+
planner,
|
|
156
|
+
writer,
|
|
157
|
+
critics,
|
|
158
|
+
verifier: { kind: "witness", command: "npm run verify" },
|
|
159
|
+
},
|
|
160
|
+
suggestedCommand: bestPattern.defaultCommand(prompt.replace(/"/g, '\\"')),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { parse, stringify } from "yaml";
|
|
5
|
+
|
|
6
|
+
export const GOAL_SCHEMA = "kxm.goal.v1" as const;
|
|
7
|
+
export const TASK_SCHEMA = "kxm.task.v1" as const;
|
|
8
|
+
|
|
9
|
+
export type GoalStatus = "active" | "achieved" | "abandoned";
|
|
10
|
+
export type TaskStatus = "todo" | "in_progress" | "blocked" | "in_review" | "done";
|
|
11
|
+
export type TrackerType = "github" | "jira" | "gitlab" | "none";
|
|
12
|
+
|
|
13
|
+
export interface GoalRecord {
|
|
14
|
+
schema: typeof GOAL_SCHEMA;
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
area: string;
|
|
18
|
+
status: GoalStatus;
|
|
19
|
+
successMetrics: string[];
|
|
20
|
+
targetDate?: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TaskAcceptanceCriterion {
|
|
26
|
+
id?: string;
|
|
27
|
+
description: string;
|
|
28
|
+
required: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TaskTrackerSync {
|
|
32
|
+
tracker: TrackerType;
|
|
33
|
+
issueKey: string;
|
|
34
|
+
syncStatus: "synced" | "pending" | "failed";
|
|
35
|
+
lastSyncedAt?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TaskRecord {
|
|
39
|
+
schema: typeof TASK_SCHEMA;
|
|
40
|
+
id: string;
|
|
41
|
+
goalId?: string;
|
|
42
|
+
title: string;
|
|
43
|
+
objective: string;
|
|
44
|
+
acceptanceCriteria: TaskAcceptanceCriterion[];
|
|
45
|
+
status: TaskStatus;
|
|
46
|
+
assignedWorkflow?: string;
|
|
47
|
+
workflowRunId?: string;
|
|
48
|
+
trackerSync?: TaskTrackerSync;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function goalsDirectory(repoRoot: string): string {
|
|
54
|
+
return resolve(repoRoot, ".kxm", "goals");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tasksDirectory(repoRoot: string): string {
|
|
58
|
+
return resolve(repoRoot, ".kxm", "tasks");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createGoal(
|
|
62
|
+
repoRoot: string,
|
|
63
|
+
input: {
|
|
64
|
+
title: string;
|
|
65
|
+
area?: string | undefined;
|
|
66
|
+
successMetrics?: string[] | undefined;
|
|
67
|
+
targetDate?: string | undefined;
|
|
68
|
+
},
|
|
69
|
+
): GoalRecord {
|
|
70
|
+
const dir = goalsDirectory(repoRoot);
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const id = `goal_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
const record: GoalRecord = {
|
|
76
|
+
schema: GOAL_SCHEMA,
|
|
77
|
+
id,
|
|
78
|
+
title: input.title,
|
|
79
|
+
area: input.area ?? "software-engineering",
|
|
80
|
+
status: "active",
|
|
81
|
+
successMetrics: input.successMetrics ?? [],
|
|
82
|
+
...(input.targetDate ? { targetDate: input.targetDate } : {}),
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const filePath = join(dir, `${id}.yaml`);
|
|
88
|
+
writeFileSync(filePath, stringify(record).trim() + "\n", "utf8");
|
|
89
|
+
return record;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function listGoals(repoRoot: string): GoalRecord[] {
|
|
93
|
+
const dir = goalsDirectory(repoRoot);
|
|
94
|
+
if (!existsSync(dir)) return [];
|
|
95
|
+
|
|
96
|
+
const goals: GoalRecord[] = [];
|
|
97
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
|
|
100
|
+
try {
|
|
101
|
+
const text = readFileSync(join(dir, entry.name), "utf8");
|
|
102
|
+
const doc = parse(text) as GoalRecord;
|
|
103
|
+
if (doc && doc.schema === GOAL_SCHEMA) goals.push(doc);
|
|
104
|
+
} catch {
|
|
105
|
+
// skip invalid
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return goals.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createTask(
|
|
114
|
+
repoRoot: string,
|
|
115
|
+
input: {
|
|
116
|
+
goalId?: string | undefined;
|
|
117
|
+
title: string;
|
|
118
|
+
objective: string;
|
|
119
|
+
acceptanceCriteria?: TaskAcceptanceCriterion[] | undefined;
|
|
120
|
+
assignedWorkflow?: string | undefined;
|
|
121
|
+
trackerSync?: {
|
|
122
|
+
tracker: TrackerType;
|
|
123
|
+
issueKey: string;
|
|
124
|
+
} | undefined;
|
|
125
|
+
},
|
|
126
|
+
): TaskRecord {
|
|
127
|
+
const dir = tasksDirectory(repoRoot);
|
|
128
|
+
mkdirSync(dir, { recursive: true });
|
|
129
|
+
|
|
130
|
+
const id = `task_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
131
|
+
const now = new Date().toISOString();
|
|
132
|
+
const record: TaskRecord = {
|
|
133
|
+
schema: TASK_SCHEMA,
|
|
134
|
+
id,
|
|
135
|
+
...(input.goalId ? { goalId: input.goalId } : {}),
|
|
136
|
+
title: input.title,
|
|
137
|
+
objective: input.objective,
|
|
138
|
+
acceptanceCriteria: input.acceptanceCriteria ?? [],
|
|
139
|
+
status: "todo",
|
|
140
|
+
...(input.assignedWorkflow ? { assignedWorkflow: input.assignedWorkflow } : {}),
|
|
141
|
+
...(input.trackerSync
|
|
142
|
+
? {
|
|
143
|
+
trackerSync: {
|
|
144
|
+
tracker: input.trackerSync.tracker,
|
|
145
|
+
issueKey: input.trackerSync.issueKey,
|
|
146
|
+
syncStatus: "pending",
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
createdAt: now,
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const filePath = join(dir, `${id}.yaml`);
|
|
155
|
+
writeFileSync(filePath, stringify(record).trim() + "\n", "utf8");
|
|
156
|
+
return record;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function listTasks(
|
|
160
|
+
repoRoot: string,
|
|
161
|
+
options: { goalId?: string | undefined; status?: TaskStatus | undefined } = {},
|
|
162
|
+
): TaskRecord[] {
|
|
163
|
+
const dir = tasksDirectory(repoRoot);
|
|
164
|
+
if (!existsSync(dir)) return [];
|
|
165
|
+
|
|
166
|
+
const tasks: TaskRecord[] = [];
|
|
167
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
|
|
170
|
+
try {
|
|
171
|
+
const text = readFileSync(join(dir, entry.name), "utf8");
|
|
172
|
+
const doc = parse(text) as TaskRecord;
|
|
173
|
+
if (doc && doc.schema === TASK_SCHEMA) {
|
|
174
|
+
if (options.goalId && doc.goalId !== options.goalId) continue;
|
|
175
|
+
if (options.status && doc.status !== options.status) continue;
|
|
176
|
+
tasks.push(doc);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// skip invalid
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getTask(repoRoot: string, taskId: string): TaskRecord | undefined {
|
|
188
|
+
const dir = tasksDirectory(repoRoot);
|
|
189
|
+
const filePath = join(dir, `${taskId}.yaml`);
|
|
190
|
+
if (!existsSync(filePath)) return undefined;
|
|
191
|
+
try {
|
|
192
|
+
const text = readFileSync(filePath, "utf8");
|
|
193
|
+
const doc = parse(text) as TaskRecord;
|
|
194
|
+
return doc && doc.schema === TASK_SCHEMA ? doc : undefined;
|
|
195
|
+
} catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function updateTaskStatus(
|
|
201
|
+
repoRoot: string,
|
|
202
|
+
taskId: string,
|
|
203
|
+
status: TaskStatus,
|
|
204
|
+
options: { workflowRunId?: string } = {},
|
|
205
|
+
): TaskRecord {
|
|
206
|
+
const task = getTask(repoRoot, taskId);
|
|
207
|
+
if (!task) {
|
|
208
|
+
throw new Error(`Task ${taskId} not found`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
task.status = status;
|
|
212
|
+
if (options.workflowRunId) task.workflowRunId = options.workflowRunId;
|
|
213
|
+
task.updatedAt = new Date().toISOString();
|
|
214
|
+
|
|
215
|
+
const filePath = join(tasksDirectory(repoRoot), `${taskId}.yaml`);
|
|
216
|
+
writeFileSync(filePath, stringify(task).trim() + "\n", "utf8");
|
|
217
|
+
return task;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function syncTaskWithTracker(
|
|
221
|
+
repoRoot: string,
|
|
222
|
+
taskId: string,
|
|
223
|
+
options: {
|
|
224
|
+
mockRemoteState?: string;
|
|
225
|
+
commentReceipt?: string;
|
|
226
|
+
} = {},
|
|
227
|
+
): TaskRecord {
|
|
228
|
+
const task = getTask(repoRoot, taskId);
|
|
229
|
+
if (!task) {
|
|
230
|
+
throw new Error(`Task ${taskId} not found`);
|
|
231
|
+
}
|
|
232
|
+
if (!task.trackerSync) {
|
|
233
|
+
throw new Error(`Task ${taskId} does not have an associated issue board tracker`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
task.trackerSync.syncStatus = "synced";
|
|
238
|
+
task.trackerSync.lastSyncedAt = now;
|
|
239
|
+
task.updatedAt = now;
|
|
240
|
+
|
|
241
|
+
const filePath = join(tasksDirectory(repoRoot), `${taskId}.yaml`);
|
|
242
|
+
writeFileSync(filePath, stringify(task).trim() + "\n", "utf8");
|
|
243
|
+
return task;
|
|
244
|
+
}
|