@kynetic-ai/spec 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/acp/client.d.ts +13 -1
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +17 -2
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +12 -1
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +27 -4
- package/dist/acp/framing.js.map +1 -1
- package/dist/agent-runtime/dispatch.d.ts +261 -0
- package/dist/agent-runtime/dispatch.d.ts.map +1 -0
- package/dist/agent-runtime/dispatch.js +791 -0
- package/dist/agent-runtime/dispatch.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +11 -0
- package/dist/agent-runtime/index.d.ts.map +1 -0
- package/dist/agent-runtime/index.js +11 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/invocation.d.ts +86 -0
- package/dist/agent-runtime/invocation.d.ts.map +1 -0
- package/dist/agent-runtime/invocation.js +442 -0
- package/dist/agent-runtime/invocation.js.map +1 -0
- package/dist/agent-runtime/prompts.d.ts +50 -0
- package/dist/agent-runtime/prompts.d.ts.map +1 -0
- package/dist/agent-runtime/prompts.js +108 -0
- package/dist/agent-runtime/prompts.js.map +1 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +60 -4
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-exec.d.ts.map +1 -1
- package/dist/cli/batch-exec.js +183 -81
- package/dist/cli/batch-exec.js.map +1 -1
- package/dist/cli/batch-write-buffer.d.ts +141 -0
- package/dist/cli/batch-write-buffer.d.ts.map +1 -0
- package/dist/cli/batch-write-buffer.js +400 -0
- package/dist/cli/batch-write-buffer.js.map +1 -0
- package/dist/cli/commands/agent.d.ts +20 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +831 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +1 -1
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +2 -1
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/batch.js +1 -1
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +46 -22
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +4 -6
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +34 -17
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/log.js +1 -1
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/merge-driver.d.ts.map +1 -1
- package/dist/cli/commands/merge-driver.js +8 -3
- package/dist/cli/commands/merge-driver.js.map +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +159 -6
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +2 -1
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.js +19 -3
- package/dist/cli/commands/plan-import.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +87 -43
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts +5 -51
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +52 -1462
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +22 -13
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +70 -11
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
- package/dist/cli/commands/session/checkpoint.js +7 -2
- package/dist/cli/commands/session/checkpoint.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +15 -0
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/context.d.ts.map +1 -1
- package/dist/cli/commands/session/context.js +10 -5
- package/dist/cli/commands/session/context.js.map +1 -1
- package/dist/cli/commands/session/log.d.ts +1 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +124 -8
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/session/stale-close.d.ts +17 -0
- package/dist/cli/commands/session/stale-close.d.ts.map +1 -0
- package/dist/cli/commands/session/stale-close.js +378 -0
- package/dist/cli/commands/session/stale-close.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +4 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +150 -6
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts.map +1 -1
- package/dist/cli/commands/skill-crud.js +4 -3
- package/dist/cli/commands/skill-crud.js.map +1 -1
- package/dist/cli/commands/skill-diff.d.ts.map +1 -1
- package/dist/cli/commands/skill-diff.js +15 -0
- package/dist/cli/commands/skill-diff.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +50 -18
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +552 -323
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.js +1 -1
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +37 -13
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +99 -50
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +5 -0
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +5 -1
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/validators.d.ts +4 -0
- package/dist/cli/validators.d.ts.map +1 -1
- package/dist/cli/validators.js +12 -0
- package/dist/cli/validators.js.map +1 -1
- package/dist/daemon/project-context.ts +22 -0
- package/dist/daemon/routes/agent-dispatch.ts +272 -0
- package/dist/daemon/server.ts +55 -20
- package/dist/daemon/websocket/handler.ts +67 -6
- package/dist/daemon/websocket/lifecycle.ts +19 -0
- package/dist/daemon/websocket/pubsub.ts +74 -3
- package/dist/export/html.d.ts.map +1 -1
- package/dist/export/html.js +5 -2
- package/dist/export/html.js.map +1 -1
- package/dist/export/triage.d.ts +1 -1
- package/dist/export/triage.d.ts.map +1 -1
- package/dist/export/triage.js +5 -3
- package/dist/export/triage.js.map +1 -1
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +6 -3
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.js +1 -1
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +6 -6
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +9 -8
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +12 -12
- package/dist/parser/plans.d.ts +7 -0
- package/dist/parser/plans.d.ts.map +1 -1
- package/dist/parser/plans.js +100 -15
- package/dist/parser/plans.js.map +1 -1
- package/dist/parser/refs.d.ts +5 -0
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +17 -12
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/shadow.d.ts +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +241 -76
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +6 -3
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +70 -108
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +24 -5
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +228 -66
- package/dist/parser/yaml.js.map +1 -1
- package/dist/schema/meta.d.ts +442 -119
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +55 -0
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +22 -22
- package/dist/schema/spec.d.ts +39 -39
- package/dist/schema/task.d.ts +43 -32
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +5 -0
- package/dist/schema/task.js.map +1 -1
- package/dist/sessions/store.d.ts +112 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +414 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +75 -17
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +51 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/triage/actions.d.ts +1 -0
- package/dist/triage/actions.d.ts.map +1 -1
- package/dist/triage/actions.js +34 -7
- package/dist/triage/actions.js.map +1 -1
- package/dist/utils/commit.js +1 -1
- package/dist/utils/commit.js.map +1 -1
- package/dist/web-ui/_app/env.js +1 -0
- package/dist/web-ui/_app/immutable/assets/0.BxCxvrZR.css +1 -0
- package/dist/web-ui/_app/immutable/assets/select-trigger.CV-KWLNP.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/B-CZR0q8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B1IR5Su5.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BCkp8Hs8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B_Cvvtc4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BtFaGGII.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Bu8JVsCH.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C87u-CNA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CrFkBTYp.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D1ArdqNb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D28BF5MJ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D6RtLpzL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D7FHSgx2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DBXrsxZQ.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/Da_hHMuA.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Do6LchSF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DoNPtcAw.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DtUbXRZz.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DyFPRlLl.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzAP8lRM.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzVXElzN.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/aoPBFken.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/i-XnOIX0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/laxtrUO3.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/q1nIWgqB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/sTLbk5Nm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/vwKgQu5P.js +5 -0
- package/dist/web-ui/_app/immutable/entry/app.BCwMcqnT.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.wKCQH-tt.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.CjGVMG74.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.B6_AIPan.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.q4oCS7Ws.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.rTKZf9o2.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.DVIDRu1d.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/5.8PtPXIOd.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.ZZrTemy_.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.IP-gxCxi.js +1 -0
- package/dist/web-ui/_app/version.json +1 -0
- package/dist/web-ui/index.html +36 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +1 -1
- package/plugin/plugins/kspec/skills/{observations → observe}/SKILL.md +1 -1
- package/plugin/plugins/kspec/skills/plan/SKILL.md +1 -1
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +26 -3
- package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +1 -1
- package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +1 -1
- package/templates/agents-sections/01-quick-start.md +1 -0
- package/templates/agents-sections/06-ralph-loop.md +64 -11
- package/templates/skills/create-workflow/SKILL.md +1 -1
- package/templates/skills/manifest.yaml +1 -1
- package/templates/skills/plan/SKILL.md +1 -1
- package/templates/skills/task-work/SKILL.md +26 -3
- package/templates/skills/triage-inbox/SKILL.md +1 -1
- package/templates/skills/writing-specs/SKILL.md +1 -1
- package/dist/ralph/cli-renderer.d.ts +0 -27
- package/dist/ralph/cli-renderer.d.ts.map +0 -1
- package/dist/ralph/cli-renderer.js +0 -250
- package/dist/ralph/cli-renderer.js.map +0 -1
- package/dist/ralph/events.d.ts +0 -65
- package/dist/ralph/events.d.ts.map +0 -1
- package/dist/ralph/events.js +0 -600
- package/dist/ralph/events.js.map +0 -1
- package/dist/ralph/index.d.ts +0 -11
- package/dist/ralph/index.d.ts.map +0 -1
- package/dist/ralph/index.js +0 -16
- package/dist/ralph/index.js.map +0 -1
- package/dist/ralph/loop-errors.d.ts +0 -83
- package/dist/ralph/loop-errors.d.ts.map +0 -1
- package/dist/ralph/loop-errors.js +0 -150
- package/dist/ralph/loop-errors.js.map +0 -1
- package/dist/ralph/subagent.d.ts +0 -96
- package/dist/ralph/subagent.d.ts.map +0 -1
- package/dist/ralph/subagent.js +0 -195
- package/dist/ralph/subagent.js.map +0 -1
- package/dist/ralph/wrap-up.d.ts +0 -127
- package/dist/ralph/wrap-up.d.ts.map +0 -1
- package/dist/ralph/wrap-up.js +0 -271
- package/dist/ralph/wrap-up.js.map +0 -1
- /package/templates/skills/{observations → observe}/SKILL.md +0 -0
package/dist/parser/yaml.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { ulid } from "ulid";
|
|
5
5
|
import * as YAML from "yaml";
|
|
6
6
|
import { withFileLock } from "./file-lock.js";
|
|
7
|
+
import { accessBufferAware, getActiveBatchBuffer, readdirBufferAware, } from "../cli/batch-write-buffer.js";
|
|
7
8
|
import { InboxFileSchema, InboxItemSchema, ManifestSchema, SpecItemSchema, TaskSchema, TasksFileSchema, TriageFileSchema, TriageRecordSchema, } from "../schema/index.js";
|
|
8
9
|
import { errors } from "../strings/index.js";
|
|
9
10
|
import { ItemIndex } from "./items.js";
|
|
@@ -68,10 +69,32 @@ export function toYaml(obj) {
|
|
|
68
69
|
return yamlString;
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* Read
|
|
72
|
+
* Read a text file with batch buffer overlay semantics.
|
|
73
|
+
*/
|
|
74
|
+
export async function readFileBufferAware(filePath) {
|
|
75
|
+
// AC: @batch-write-buffer ac-2 — check buffer first for read-after-write consistency
|
|
76
|
+
const buffer = getActiveBatchBuffer();
|
|
77
|
+
if (buffer?.isInScope(filePath)) {
|
|
78
|
+
const buffered = buffer.read(filePath);
|
|
79
|
+
if (buffered !== undefined) {
|
|
80
|
+
if (buffered === null) {
|
|
81
|
+
// File was deleted in this batch
|
|
82
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${filePath}'`), {
|
|
83
|
+
code: "ENOENT",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return typeof buffered === "string"
|
|
87
|
+
? buffered
|
|
88
|
+
: Buffer.from(buffered).toString("utf-8");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return fs.readFile(filePath, "utf-8");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read and parse a YAML file.
|
|
72
95
|
*/
|
|
73
96
|
export async function readYamlFile(filePath) {
|
|
74
|
-
const content = await
|
|
97
|
+
const content = await readFileBufferAware(filePath);
|
|
75
98
|
return parseYaml(content);
|
|
76
99
|
}
|
|
77
100
|
/**
|
|
@@ -79,6 +102,12 @@ export async function readYamlFile(filePath) {
|
|
|
79
102
|
*/
|
|
80
103
|
export async function writeYamlFile(filePath, data) {
|
|
81
104
|
const content = toYaml(data);
|
|
105
|
+
// AC: @batch-write-buffer ac-1 — buffer write if in batch mode
|
|
106
|
+
const buffer = getActiveBatchBuffer();
|
|
107
|
+
if (buffer?.isInScope(filePath)) {
|
|
108
|
+
buffer.write(filePath, content);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
82
111
|
await fs.writeFile(filePath, content, "utf-8");
|
|
83
112
|
}
|
|
84
113
|
/**
|
|
@@ -90,6 +119,12 @@ export async function writeYamlFile(filePath, data) {
|
|
|
90
119
|
*/
|
|
91
120
|
export async function writeYamlFilePreserveFormat(filePath, data) {
|
|
92
121
|
const content = toYaml(data);
|
|
122
|
+
// AC: @batch-write-buffer ac-1 — buffer write if in batch mode
|
|
123
|
+
const buffer = getActiveBatchBuffer();
|
|
124
|
+
if (buffer?.isInScope(filePath)) {
|
|
125
|
+
buffer.write(filePath, content);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
93
128
|
await fs.writeFile(filePath, content, "utf-8");
|
|
94
129
|
}
|
|
95
130
|
/**
|
|
@@ -98,7 +133,7 @@ export async function writeYamlFilePreserveFormat(filePath, data) {
|
|
|
98
133
|
export async function findTaskFiles(dir) {
|
|
99
134
|
const files = [];
|
|
100
135
|
try {
|
|
101
|
-
const entries = await
|
|
136
|
+
const entries = await readdirBufferAware(dir, { withFileTypes: true });
|
|
102
137
|
for (const entry of entries) {
|
|
103
138
|
const fullPath = path.join(dir, entry.name);
|
|
104
139
|
if (entry.isDirectory()) {
|
|
@@ -292,7 +327,7 @@ async function findManifestInDir(dir) {
|
|
|
292
327
|
for (const candidate of priorityCandidates) {
|
|
293
328
|
const filePath = path.join(dir, candidate);
|
|
294
329
|
try {
|
|
295
|
-
await
|
|
330
|
+
await accessBufferAware(filePath);
|
|
296
331
|
return filePath;
|
|
297
332
|
}
|
|
298
333
|
catch {
|
|
@@ -301,7 +336,7 @@ async function findManifestInDir(dir) {
|
|
|
301
336
|
}
|
|
302
337
|
// AC: @manifest-discovery ac-3, ac-4, ac-5 - glob fallback with validation
|
|
303
338
|
try {
|
|
304
|
-
const entries = await
|
|
339
|
+
const entries = await readdirBufferAware(dir);
|
|
305
340
|
// AC: @manifest-discovery ac-4 - alphabetical order
|
|
306
341
|
const candidates = entries.filter(isManifestCandidate).sort();
|
|
307
342
|
for (const candidate of candidates) {
|
|
@@ -373,8 +408,10 @@ async function loadTasksFromFile(filePath) {
|
|
|
373
408
|
*/
|
|
374
409
|
export async function loadAllTasks(ctx) {
|
|
375
410
|
const tasks = [];
|
|
376
|
-
// When shadow is enabled, look only in specDir
|
|
377
|
-
|
|
411
|
+
// When shadow is enabled (or spec dir is explicitly overridden), look only in specDir.
|
|
412
|
+
// KSPEC_SPEC_DIR override is used by batch mode and some integration tests to isolate
|
|
413
|
+
// task state to a temp directory; scanning ctx.rootDir can leak tasks from parent dirs.
|
|
414
|
+
if (ctx.shadow?.enabled || Boolean(process.env.KSPEC_SPEC_DIR)) {
|
|
378
415
|
const taskFiles = await findTaskFiles(ctx.specDir);
|
|
379
416
|
// Also check for standalone files in specDir
|
|
380
417
|
const standaloneLocations = [
|
|
@@ -386,7 +423,7 @@ export async function loadAllTasks(ctx) {
|
|
|
386
423
|
];
|
|
387
424
|
for (const loc of standaloneLocations) {
|
|
388
425
|
try {
|
|
389
|
-
await
|
|
426
|
+
await accessBufferAware(loc);
|
|
390
427
|
if (!taskFiles.includes(loc)) {
|
|
391
428
|
taskFiles.push(loc);
|
|
392
429
|
}
|
|
@@ -424,7 +461,7 @@ export async function loadAllTasks(ctx) {
|
|
|
424
461
|
];
|
|
425
462
|
for (const loc of standaloneLocations) {
|
|
426
463
|
try {
|
|
427
|
-
await
|
|
464
|
+
await accessBufferAware(loc);
|
|
428
465
|
if (!taskFiles.includes(loc)) {
|
|
429
466
|
taskFiles.push(loc);
|
|
430
467
|
}
|
|
@@ -554,6 +591,58 @@ export async function saveTask(ctx, task) {
|
|
|
554
591
|
}
|
|
555
592
|
});
|
|
556
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Atomically mutate a task using the latest on-disk state.
|
|
596
|
+
*
|
|
597
|
+
* The callback receives the current task value while holding the task file lock,
|
|
598
|
+
* so concurrent writers cannot clobber unrelated fields (for example status vs notes).
|
|
599
|
+
*/
|
|
600
|
+
export async function mutateTaskAtomically(ctx, task, mutate) {
|
|
601
|
+
const taskFilePath = task._sourceFile || getDefaultTaskFilePath(ctx);
|
|
602
|
+
let updatedTask;
|
|
603
|
+
await withFileLock(taskFilePath, async () => {
|
|
604
|
+
// Ensure directory exists (important for default path in new repos)
|
|
605
|
+
const dir = path.dirname(taskFilePath);
|
|
606
|
+
await fs.mkdir(dir, { recursive: true });
|
|
607
|
+
// Preserve existing file format (tasks wrapper vs plain array)
|
|
608
|
+
let existingRaw = null;
|
|
609
|
+
let useTasksWrapper = false;
|
|
610
|
+
try {
|
|
611
|
+
existingRaw = await readYamlFile(taskFilePath);
|
|
612
|
+
if (existingRaw &&
|
|
613
|
+
typeof existingRaw === "object" &&
|
|
614
|
+
"tasks" in existingRaw) {
|
|
615
|
+
useTasksWrapper = true;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
throw new Error(`Task file not found: ${taskFilePath}`);
|
|
620
|
+
}
|
|
621
|
+
const fileTasks = await loadTasksFromFile(taskFilePath);
|
|
622
|
+
const taskIndex = fileTasks.findIndex((t) => t._ulid === task._ulid);
|
|
623
|
+
if (taskIndex === -1) {
|
|
624
|
+
throw new Error(`Task not found in file: ${task._ulid}`);
|
|
625
|
+
}
|
|
626
|
+
const latestTask = fileTasks[taskIndex];
|
|
627
|
+
const mutatedTask = await mutate(latestTask);
|
|
628
|
+
const cleanMutatedTask = stripRuntimeMetadata(mutatedTask);
|
|
629
|
+
const serializedTasks = fileTasks.map((fileTask, index) => index === taskIndex ? cleanMutatedTask : stripRuntimeMetadata(fileTask));
|
|
630
|
+
if (useTasksWrapper) {
|
|
631
|
+
await writeYamlFilePreserveFormat(taskFilePath, { tasks: serializedTasks });
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
await writeYamlFilePreserveFormat(taskFilePath, serializedTasks);
|
|
635
|
+
}
|
|
636
|
+
updatedTask = {
|
|
637
|
+
...cleanMutatedTask,
|
|
638
|
+
_sourceFile: taskFilePath,
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
if (!updatedTask) {
|
|
642
|
+
throw new Error(`Failed to mutate task atomically: ${task._ulid}`);
|
|
643
|
+
}
|
|
644
|
+
return updatedTask;
|
|
645
|
+
}
|
|
557
646
|
/**
|
|
558
647
|
* Delete a task from its source file.
|
|
559
648
|
* Requires _sourceFile to know which file to modify.
|
|
@@ -736,28 +825,34 @@ export function areDependenciesMet(task, allTasks) {
|
|
|
736
825
|
return true;
|
|
737
826
|
}
|
|
738
827
|
/**
|
|
739
|
-
* Check if task is ready (pending + deps met + not blocked)
|
|
828
|
+
* Check if task is ready (pending/needs_work + deps met + not blocked)
|
|
740
829
|
*/
|
|
741
830
|
export function isTaskReady(task, allTasks) {
|
|
742
|
-
if (task.status !== "pending")
|
|
831
|
+
if (task.status !== "pending" && task.status !== "needs_work")
|
|
743
832
|
return false;
|
|
744
833
|
if (task.blocked_by.length > 0)
|
|
745
834
|
return false;
|
|
746
835
|
return areDependenciesMet(task, allTasks);
|
|
747
836
|
}
|
|
748
837
|
/**
|
|
749
|
-
* Get ready tasks (pending + deps met + not blocked), sorted by
|
|
750
|
-
*
|
|
838
|
+
* Get ready tasks (pending/needs_work + deps met + not blocked), sorted by
|
|
839
|
+
* status (needs_work first), then priority, then creation time.
|
|
840
|
+
* Within the same tier, older tasks come first (FIFO).
|
|
751
841
|
*/
|
|
752
842
|
export function getReadyTasks(tasks) {
|
|
753
843
|
return tasks
|
|
754
844
|
.filter((task) => isTaskReady(task, tasks))
|
|
755
845
|
.sort((a, b) => {
|
|
756
|
-
// Primary:
|
|
846
|
+
// Primary: needs_work before pending (fix cycles take priority)
|
|
847
|
+
const statusOrder = (s) => (s === "needs_work" ? 0 : 1);
|
|
848
|
+
const statusDiff = statusOrder(a.status) - statusOrder(b.status);
|
|
849
|
+
if (statusDiff !== 0)
|
|
850
|
+
return statusDiff;
|
|
851
|
+
// Secondary: priority (lower number = higher priority)
|
|
757
852
|
if (a.priority !== b.priority) {
|
|
758
853
|
return a.priority - b.priority;
|
|
759
854
|
}
|
|
760
|
-
//
|
|
855
|
+
// Tertiary: creation time (older first - FIFO within priority)
|
|
761
856
|
return (new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
762
857
|
});
|
|
763
858
|
}
|
|
@@ -775,7 +870,7 @@ export async function expandIncludePattern(pattern, baseDir) {
|
|
|
775
870
|
// If no glob characters, just return the path if it exists
|
|
776
871
|
if (!pattern.includes("*")) {
|
|
777
872
|
try {
|
|
778
|
-
await
|
|
873
|
+
await accessBufferAware(fullPattern);
|
|
779
874
|
return [fullPattern];
|
|
780
875
|
}
|
|
781
876
|
catch {
|
|
@@ -805,7 +900,7 @@ async function expandGlobRecursive(dir, pattern, result) {
|
|
|
805
900
|
const currentPattern = parts[0];
|
|
806
901
|
const remainingPattern = parts.slice(1).join("/");
|
|
807
902
|
try {
|
|
808
|
-
const entries = await
|
|
903
|
+
const entries = await readdirBufferAware(dir, { withFileTypes: true });
|
|
809
904
|
for (const entry of entries) {
|
|
810
905
|
const matches = matchGlobPart(entry.name, currentPattern);
|
|
811
906
|
if (matches) {
|
|
@@ -948,7 +1043,7 @@ export function extractItemsFromRaw(raw, sourceFile, items = [], currentPath = "
|
|
|
948
1043
|
*/
|
|
949
1044
|
export async function loadSpecFile(filePath) {
|
|
950
1045
|
try {
|
|
951
|
-
const content = await
|
|
1046
|
+
const content = await readFileBufferAware(filePath);
|
|
952
1047
|
const items = [];
|
|
953
1048
|
// Parse all YAML documents in the file (handles files with ---)
|
|
954
1049
|
const documents = YAML.parseAllDocuments(content);
|
|
@@ -1058,6 +1153,12 @@ function stripSpecItemMetadata(item) {
|
|
|
1058
1153
|
const { _sourceFile, _path, ...cleanItem } = item;
|
|
1059
1154
|
return cleanItem;
|
|
1060
1155
|
}
|
|
1156
|
+
function assertSpecItemPatch(updates, operation) {
|
|
1157
|
+
const patch = updates;
|
|
1158
|
+
if ("_sourceFile" in patch || "_path" in patch) {
|
|
1159
|
+
throw new Error(`${operation} expects a patch object, not a full LoadedSpecItem. Pass only intended fields to update.`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1061
1162
|
/**
|
|
1062
1163
|
* Parse a path string into segments.
|
|
1063
1164
|
* e.g., "features[0].requirements[2]" -> [["features", 0], ["requirements", 2]]
|
|
@@ -1225,6 +1326,7 @@ export async function updateSpecItem(_ctx, item, updates) {
|
|
|
1225
1326
|
if (!item._sourceFile) {
|
|
1226
1327
|
throw new Error("Item has no source file");
|
|
1227
1328
|
}
|
|
1329
|
+
assertSpecItemPatch(updates, "updateSpecItem");
|
|
1228
1330
|
// Lock the file to prevent concurrent read-modify-write races
|
|
1229
1331
|
return withFileLock(item._sourceFile, async () => {
|
|
1230
1332
|
// Load the raw YAML
|
|
@@ -1233,10 +1335,19 @@ export async function updateSpecItem(_ctx, item, updates) {
|
|
|
1233
1335
|
let targetObj;
|
|
1234
1336
|
if (item._path) {
|
|
1235
1337
|
const nav = navigateToPath(raw, item._path);
|
|
1236
|
-
|
|
1237
|
-
|
|
1338
|
+
const candidate = nav?.array[nav.index];
|
|
1339
|
+
if (candidate &&
|
|
1340
|
+
typeof candidate === "object" &&
|
|
1341
|
+
candidate._ulid === item._ulid) {
|
|
1342
|
+
targetObj = candidate;
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
const found = findItemInStructure(raw, item._ulid);
|
|
1346
|
+
if (!found) {
|
|
1347
|
+
throw new Error(`Could not find item ${item._ulid} in structure (path: ${item._path})`);
|
|
1348
|
+
}
|
|
1349
|
+
targetObj = found.item;
|
|
1238
1350
|
}
|
|
1239
|
-
targetObj = nav.array[nav.index];
|
|
1240
1351
|
}
|
|
1241
1352
|
else {
|
|
1242
1353
|
// Item might be the root, or we need to find it
|
|
@@ -1334,10 +1445,14 @@ export async function deleteSpecItem(_ctx, item) {
|
|
|
1334
1445
|
* Save a spec item - either updates existing or adds to parent.
|
|
1335
1446
|
* For new items, use addChildItem instead.
|
|
1336
1447
|
*/
|
|
1337
|
-
export async function saveSpecItem(ctx, item) {
|
|
1448
|
+
export async function saveSpecItem(ctx, item, updates) {
|
|
1449
|
+
assertSpecItemPatch(updates, "saveSpecItem");
|
|
1450
|
+
if (Object.keys(updates).length === 0) {
|
|
1451
|
+
throw new Error("Cannot save spec item without updates. Pass a patch.");
|
|
1452
|
+
}
|
|
1338
1453
|
// If item has a source file and path, it's an update
|
|
1339
1454
|
if (item._sourceFile && item._path) {
|
|
1340
|
-
await updateSpecItem(ctx, item,
|
|
1455
|
+
await updateSpecItem(ctx, item, updates);
|
|
1341
1456
|
return;
|
|
1342
1457
|
}
|
|
1343
1458
|
// Otherwise, this is more complex - would need a parent
|
|
@@ -1353,34 +1468,60 @@ export function getInboxFilePath(ctx) {
|
|
|
1353
1468
|
return path.join(ctx.specDir, "project.inbox.yaml");
|
|
1354
1469
|
}
|
|
1355
1470
|
/**
|
|
1356
|
-
*
|
|
1471
|
+
* Parse inbox items from raw YAML payload.
|
|
1472
|
+
*
|
|
1473
|
+
* Supports canonical { inbox: [...] } shape and legacy plain-array shape.
|
|
1357
1474
|
*/
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
...item,
|
|
1368
|
-
_sourceFile: inboxPath,
|
|
1369
|
-
}));
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
// Handle plain array format
|
|
1373
|
-
if (Array.isArray(raw)) {
|
|
1475
|
+
function parseInboxItemsFromRaw(raw) {
|
|
1476
|
+
// Handle { inbox: [...] } format
|
|
1477
|
+
if (raw && typeof raw === "object" && "inbox" in raw) {
|
|
1478
|
+
const parsed = InboxFileSchema.safeParse(raw);
|
|
1479
|
+
if (parsed.success) {
|
|
1480
|
+
return parsed.data.inbox;
|
|
1481
|
+
}
|
|
1482
|
+
const fallbackItems = raw.inbox;
|
|
1483
|
+
if (Array.isArray(fallbackItems)) {
|
|
1374
1484
|
const items = [];
|
|
1375
|
-
for (const item of
|
|
1485
|
+
for (const item of fallbackItems) {
|
|
1376
1486
|
const result = InboxItemSchema.safeParse(item);
|
|
1377
1487
|
if (result.success) {
|
|
1378
|
-
items.push(
|
|
1488
|
+
items.push(result.data);
|
|
1379
1489
|
}
|
|
1380
1490
|
}
|
|
1381
1491
|
return items;
|
|
1382
1492
|
}
|
|
1383
|
-
|
|
1493
|
+
}
|
|
1494
|
+
// Handle plain array format
|
|
1495
|
+
if (Array.isArray(raw)) {
|
|
1496
|
+
const items = [];
|
|
1497
|
+
for (const item of raw) {
|
|
1498
|
+
const result = InboxItemSchema.safeParse(item);
|
|
1499
|
+
if (result.success) {
|
|
1500
|
+
items.push(result.data);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return items;
|
|
1504
|
+
}
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Load inbox items from an explicit file path.
|
|
1509
|
+
*/
|
|
1510
|
+
async function loadInboxItemsFromFile(inboxPath) {
|
|
1511
|
+
const raw = await readYamlFile(inboxPath);
|
|
1512
|
+
return parseInboxItemsFromRaw(raw);
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Load all inbox items from the project.
|
|
1516
|
+
*/
|
|
1517
|
+
export async function loadInboxItems(ctx) {
|
|
1518
|
+
const inboxPath = getInboxFilePath(ctx);
|
|
1519
|
+
try {
|
|
1520
|
+
const items = await loadInboxItemsFromFile(inboxPath);
|
|
1521
|
+
return items.map((item) => ({
|
|
1522
|
+
...item,
|
|
1523
|
+
_sourceFile: inboxPath,
|
|
1524
|
+
}));
|
|
1384
1525
|
}
|
|
1385
1526
|
catch {
|
|
1386
1527
|
// File doesn't exist or parse error
|
|
@@ -1424,21 +1565,7 @@ export async function saveInboxItem(ctx, item) {
|
|
|
1424
1565
|
// Load existing items
|
|
1425
1566
|
let existingItems = [];
|
|
1426
1567
|
try {
|
|
1427
|
-
|
|
1428
|
-
if (raw && typeof raw === "object" && "inbox" in raw) {
|
|
1429
|
-
const parsed = InboxFileSchema.safeParse(raw);
|
|
1430
|
-
if (parsed.success) {
|
|
1431
|
-
existingItems = parsed.data.inbox;
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
else if (Array.isArray(raw)) {
|
|
1435
|
-
for (const i of raw) {
|
|
1436
|
-
const result = InboxItemSchema.safeParse(i);
|
|
1437
|
-
if (result.success) {
|
|
1438
|
-
existingItems.push(result.data);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1568
|
+
existingItems = await loadInboxItemsFromFile(inboxPath);
|
|
1442
1569
|
}
|
|
1443
1570
|
catch {
|
|
1444
1571
|
// File doesn't exist, start fresh
|
|
@@ -1456,6 +1583,48 @@ export async function saveInboxItem(ctx, item) {
|
|
|
1456
1583
|
await writeYamlFilePreserveFormat(inboxPath, { inbox: existingItems });
|
|
1457
1584
|
});
|
|
1458
1585
|
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Atomically mutate an inbox item using the latest on-disk state.
|
|
1588
|
+
*
|
|
1589
|
+
* The callback receives the current item value while holding the inbox file lock,
|
|
1590
|
+
* so concurrent writers do not clobber unrelated fields (for example text vs tags).
|
|
1591
|
+
*/
|
|
1592
|
+
export async function mutateInboxItemAtomically(ctx, item, mutate) {
|
|
1593
|
+
const inboxPath = item._sourceFile || getInboxFilePath(ctx);
|
|
1594
|
+
let updatedItem;
|
|
1595
|
+
await withFileLock(inboxPath, async () => {
|
|
1596
|
+
// Ensure directory exists (important for default path in new repos)
|
|
1597
|
+
const dir = path.dirname(inboxPath);
|
|
1598
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1599
|
+
let existingItems = [];
|
|
1600
|
+
try {
|
|
1601
|
+
existingItems = await loadInboxItemsFromFile(inboxPath);
|
|
1602
|
+
}
|
|
1603
|
+
catch {
|
|
1604
|
+
throw new Error(`Inbox file not found: ${inboxPath}`);
|
|
1605
|
+
}
|
|
1606
|
+
const itemIndex = existingItems.findIndex((existingItem) => existingItem._ulid === item._ulid);
|
|
1607
|
+
if (itemIndex === -1) {
|
|
1608
|
+
throw new Error(`Inbox item not found in file: ${item._ulid}`);
|
|
1609
|
+
}
|
|
1610
|
+
const latestItem = {
|
|
1611
|
+
...existingItems[itemIndex],
|
|
1612
|
+
_sourceFile: inboxPath,
|
|
1613
|
+
};
|
|
1614
|
+
const mutatedItem = await mutate(latestItem);
|
|
1615
|
+
const cleanMutatedItem = stripInboxMetadata(mutatedItem);
|
|
1616
|
+
existingItems[itemIndex] = cleanMutatedItem;
|
|
1617
|
+
await writeYamlFilePreserveFormat(inboxPath, { inbox: existingItems });
|
|
1618
|
+
updatedItem = {
|
|
1619
|
+
...cleanMutatedItem,
|
|
1620
|
+
_sourceFile: inboxPath,
|
|
1621
|
+
};
|
|
1622
|
+
});
|
|
1623
|
+
if (!updatedItem) {
|
|
1624
|
+
throw new Error(`Failed to mutate inbox item atomically: ${item._ulid}`);
|
|
1625
|
+
}
|
|
1626
|
+
return updatedItem;
|
|
1627
|
+
}
|
|
1459
1628
|
/**
|
|
1460
1629
|
* Delete an inbox item by ULID.
|
|
1461
1630
|
*/
|
|
@@ -1464,14 +1633,7 @@ export async function deleteInboxItem(ctx, ulid) {
|
|
|
1464
1633
|
// Lock the file to prevent concurrent read-modify-write races
|
|
1465
1634
|
return withFileLock(inboxPath, async () => {
|
|
1466
1635
|
try {
|
|
1467
|
-
const
|
|
1468
|
-
let existingItems = [];
|
|
1469
|
-
if (raw && typeof raw === "object" && "inbox" in raw) {
|
|
1470
|
-
const parsed = InboxFileSchema.safeParse(raw);
|
|
1471
|
-
if (parsed.success) {
|
|
1472
|
-
existingItems = parsed.data.inbox;
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1636
|
+
const existingItems = await loadInboxItemsFromFile(inboxPath);
|
|
1475
1637
|
const index = existingItems.findIndex((i) => i._ulid === ulid);
|
|
1476
1638
|
if (index < 0) {
|
|
1477
1639
|
return false;
|