@kynetic-ai/spec 0.9.1 → 0.11.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 +6 -1
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +7 -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 +292 -0
- package/dist/agent-runtime/dispatch.d.ts.map +1 -0
- package/dist/agent-runtime/dispatch.js +860 -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 +140 -62
- 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/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/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +22 -16
- 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/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +168 -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 -56
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +52 -1502
- 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.map +1 -1
- package/dist/cli/commands/setup.js +95 -0
- 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 +536 -310
- 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 +65 -25
- 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/daemon/project-context.ts +22 -0
- package/dist/daemon/routes/agent-dispatch.ts +279 -0
- package/dist/daemon/routes/items.ts +22 -0
- package/dist/daemon/routes/meta.ts +141 -1
- package/dist/daemon/routes/plans.ts +147 -0
- package/dist/daemon/routes/sessions.ts +180 -0
- package/dist/daemon/routes/tasks.ts +198 -0
- package/dist/daemon/routes/validation.ts +1 -1
- package/dist/daemon/server.ts +77 -21
- 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 +10 -5
- 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 +71 -4
- 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 +35 -76
- 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 +224 -64
- package/dist/parser/yaml.js.map +1 -1
- package/dist/schema/meta.d.ts +457 -119
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +56 -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 +126 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +440 -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.BJaYkGW2.css +1 -0
- package/dist/web-ui/_app/immutable/assets/9.SzGLxi4x.css +1 -0
- package/dist/web-ui/_app/immutable/assets/select-trigger.CV-KWLNP.css +1 -0
- package/dist/web-ui/_app/immutable/chunks/-lc0BifF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/62JVKtnb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/8RBjHMN1.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5LJFxqa.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B5wTVqxm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B6VSmczZ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/B8a0xDxR.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BEOQc37C.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BHtYorjv.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BJ0JX3ea.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BMuCqDX8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BP352uRn.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BUZujXJ2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BVA9Exy-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BWET-efb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BXkNecpt.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BYzrIfX8.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BkOJ8DkV.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BpuwufMc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BwMO4RrG.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/BysXJlZb.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C076q4JN.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/C33JaVbg.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CGtqifKp.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CHDZZ7OG.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CPPfDSei.js +5 -0
- package/dist/web-ui/_app/immutable/chunks/CUir3f4J.js +60 -0
- package/dist/web-ui/_app/immutable/chunks/Cncwi6fQ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CrCIbn0C.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/CwELQvbx.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D3vxvonu.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D6TVmR9T.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D7LTux4W.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D82RulSH.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/D9QNBZM2.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DAMmvwn4.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DAh4Wfku.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DAx07bEQ.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DBYE9jOd.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DOno4cA2.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DQA8NZIH.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DRfPm2bo.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DhQhksaB.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DjG7s6hm.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DjcCz-PU.js +2 -0
- package/dist/web-ui/_app/immutable/chunks/DkltRNvh.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DlaTnPKL.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DvA-KON-.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DxCk-KHc.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/DzO4hlg9.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/Eo4gF7ih.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/ExCq5swK.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/T3zZGv51.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/XZumBYeP.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/_ySfNjkF.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/iEtR5cV6.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/k_Qegko0.js +1 -0
- package/dist/web-ui/_app/immutable/chunks/pE6cYWlS.js +1 -0
- package/dist/web-ui/_app/immutable/entry/app.Cgu6uKeS.js +2 -0
- package/dist/web-ui/_app/immutable/entry/start.9XifnLoB.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/0.DISwcKSK.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/1.Cx2Ufqp1.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/10.C3z8ijXL.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/11.DZdIjZmM.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/12.FsIGfAOa.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/13.DZoFwagf.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/14.DaIzDKbQ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/15.BYyt4XWF.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/16.CQkSqpOe.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/2.Bkf_j2UJ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/3.kaMCurJG.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/4.BSsFPTHG.js +2 -0
- package/dist/web-ui/_app/immutable/nodes/5.CpPlcCEZ.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/6.BN4FqQmY.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/7.9kBYIZik.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/8.BuijtZ6B.js +1 -0
- package/dist/web-ui/_app/immutable/nodes/9.C-Weba8R.js +1 -0
- package/dist/web-ui/_app/version.json +1 -0
- package/dist/web-ui/index.html +39 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +4 -2
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +25 -2
- package/templates/agents-sections/06-ralph-loop.md +64 -11
- package/templates/skills/task-work/SKILL.md +25 -2
- 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 -127
- package/dist/ralph/subagent.d.ts.map +0 -1
- package/dist/ralph/subagent.js +0 -268
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { markMutating } from "../command-annotations.js";
|
|
3
|
-
import { checkSlugUniqueness, createNote, createTask, createTodo, deleteTask, getAuthor, initContext, loadAllItems, loadAllTasks, ReferenceIndex, saveTask, scanTestCoverage, syncSpecImplementationStatus, } from "../../parser/index.js";
|
|
3
|
+
import { checkSlugUniqueness, createNote, createTask, createTodo, deleteTask, getAuthor, initContext, loadAllItems, loadAllTasks, mutateTaskAtomically, ReferenceIndex, saveTask, scanTestCoverage, syncSpecImplementationStatus, } from "../../parser/index.js";
|
|
4
4
|
import { commitIfShadow } from "../../parser/shadow.js";
|
|
5
5
|
import { normalizeRefInput } from "../../schema/index.js";
|
|
6
6
|
import { alignmentCheck, errors } from "../../strings/index.js";
|
|
@@ -13,6 +13,55 @@ import { parsePriority, validateEnumOption, validateSpecRef, } from "../validato
|
|
|
13
13
|
import { addListOptions, listTasksAction } from "./tasks.js";
|
|
14
14
|
import { findClosestCommand } from "../suggest.js";
|
|
15
15
|
import { checkBudget, incrementBudget, isEndLoopRequested } from "../../sessions/store.js";
|
|
16
|
+
import { PidFileManager } from "../pid-utils.js";
|
|
17
|
+
/**
|
|
18
|
+
* Post a task state change event to the daemon dispatch engine.
|
|
19
|
+
* Fails silently — dispatch requires a running daemon; if absent, this is a no-op.
|
|
20
|
+
* AC: @daemon-agent-dispatch ac-2, ac-7
|
|
21
|
+
* AC: @agent-dispatch-engine ac-18
|
|
22
|
+
*/
|
|
23
|
+
async function postDispatchEvent(opts) {
|
|
24
|
+
// AC: @agent-dispatch-engine ac-18 - Suppress self-triggering when running
|
|
25
|
+
// inside a dispatched agent invocation. The file watcher will independently
|
|
26
|
+
// detect the change, so the CLI event would be redundant and causes stale
|
|
27
|
+
// queue entries to accumulate.
|
|
28
|
+
// NOTE: This relies on the daemon's file watcher being active. If the watcher
|
|
29
|
+
// is temporarily down, dispatched agents' task mutations won't produce dispatch
|
|
30
|
+
// events until the watcher recovers and diffs the changed state.
|
|
31
|
+
if (process.env.KSPEC_SESSION_ID)
|
|
32
|
+
return;
|
|
33
|
+
const pidManager = new PidFileManager();
|
|
34
|
+
if (!pidManager.isDaemonRunning())
|
|
35
|
+
return;
|
|
36
|
+
let port;
|
|
37
|
+
try {
|
|
38
|
+
port = pidManager.readPort();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return; // Daemon running but port unreadable — silent fail
|
|
42
|
+
}
|
|
43
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
44
|
+
if (opts.projectPath) {
|
|
45
|
+
headers['X-Kspec-Dir'] = opts.projectPath;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await fetch(`http://localhost:${port}/api/agent/events`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers,
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
task_id: opts.taskId,
|
|
53
|
+
task_ref: opts.taskRef,
|
|
54
|
+
from_status: opts.fromStatus,
|
|
55
|
+
to_status: opts.toStatus,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
}),
|
|
58
|
+
signal: AbortSignal.timeout(1000), // 1s timeout — fire-and-forget
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Silent fail — daemon unreachable or dispatch engine not running
|
|
63
|
+
}
|
|
64
|
+
}
|
|
16
65
|
/**
|
|
17
66
|
* Find a task by reference with detailed error reporting.
|
|
18
67
|
* Returns the task or exits with appropriate error.
|
|
@@ -97,20 +146,11 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
97
146
|
};
|
|
98
147
|
}
|
|
99
148
|
}
|
|
100
|
-
// Build updated task with only provided options
|
|
101
|
-
const updatedTask = { ...foundTask };
|
|
102
149
|
const changes = [];
|
|
103
|
-
|
|
104
|
-
updatedTask.title = options.title;
|
|
105
|
-
changes.push("title");
|
|
106
|
-
}
|
|
150
|
+
let noChangesMessage;
|
|
107
151
|
if (options.specRef !== undefined) {
|
|
108
152
|
// Handle 'null' string to clear spec_ref
|
|
109
|
-
if (options.specRef
|
|
110
|
-
updatedTask.spec_ref = null;
|
|
111
|
-
changes.push("spec_ref: cleared");
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
153
|
+
if (options.specRef !== "null") {
|
|
114
154
|
// Validate the spec ref exists and is a spec item
|
|
115
155
|
const specResult = index.resolve(options.specRef);
|
|
116
156
|
if (!specResult.ok) {
|
|
@@ -127,17 +167,11 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
127
167
|
error: errors.reference.specRefIsTask(options.specRef),
|
|
128
168
|
};
|
|
129
169
|
}
|
|
130
|
-
updatedTask.spec_ref = normalizeRefInput(options.specRef);
|
|
131
|
-
changes.push("spec_ref");
|
|
132
170
|
}
|
|
133
171
|
}
|
|
134
172
|
if (options.metaRef !== undefined) {
|
|
135
173
|
// Handle 'null' string to clear meta_ref
|
|
136
|
-
if (options.metaRef
|
|
137
|
-
updatedTask.meta_ref = null;
|
|
138
|
-
changes.push("meta_ref: cleared");
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
174
|
+
if (options.metaRef !== "null") {
|
|
141
175
|
// Validate the meta ref exists and is a meta item
|
|
142
176
|
const metaRefResult = index.resolve(options.metaRef);
|
|
143
177
|
if (!metaRefResult.ok) {
|
|
@@ -155,17 +189,11 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
155
189
|
error: errors.reference.metaRefPointsToSpec(options.metaRef),
|
|
156
190
|
};
|
|
157
191
|
}
|
|
158
|
-
updatedTask.meta_ref = normalizeRefInput(options.metaRef);
|
|
159
|
-
changes.push("meta_ref");
|
|
160
192
|
}
|
|
161
193
|
}
|
|
162
194
|
if (options.planRef !== undefined) {
|
|
163
195
|
// Handle 'null' string to clear plan_ref
|
|
164
|
-
if (options.planRef
|
|
165
|
-
updatedTask.plan_ref = null;
|
|
166
|
-
changes.push("plan_ref: cleared");
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
196
|
+
if (options.planRef !== "null") {
|
|
169
197
|
// First check if it's a task or spec item (wrong type)
|
|
170
198
|
const cleanRef = options.planRef.startsWith("@")
|
|
171
199
|
? options.planRef.slice(1)
|
|
@@ -191,32 +219,26 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
191
219
|
error: `Plan reference not found: ${options.planRef}`,
|
|
192
220
|
};
|
|
193
221
|
}
|
|
194
|
-
updatedTask.plan_ref = normalizeRefInput(options.planRef);
|
|
195
|
-
changes.push("plan_ref");
|
|
196
222
|
}
|
|
197
223
|
}
|
|
224
|
+
// Validate review URL if provided and not clearing
|
|
225
|
+
if (options.reviewUrl !== undefined && options.reviewUrl !== "null") {
|
|
226
|
+
try {
|
|
227
|
+
new URL(options.reviewUrl);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { success: false, error: `Invalid review URL: ${options.reviewUrl}` };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
let parsedPriority;
|
|
198
234
|
if (options.priority) {
|
|
199
235
|
const priorityResult = parsePriority(options.priority);
|
|
200
236
|
if (!priorityResult.ok) {
|
|
201
237
|
return { success: false, error: priorityResult.error };
|
|
202
238
|
}
|
|
203
|
-
|
|
204
|
-
changes.push("priority");
|
|
205
|
-
}
|
|
206
|
-
if (options.slug) {
|
|
207
|
-
if (!updatedTask.slugs.includes(options.slug)) {
|
|
208
|
-
updatedTask.slugs = [...updatedTask.slugs, options.slug];
|
|
209
|
-
changes.push("slug");
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (options.tag) {
|
|
213
|
-
const parsedTags = parseTagsArray(options.tag);
|
|
214
|
-
const newTags = parsedTags.filter((t) => !updatedTask.tags.includes(t));
|
|
215
|
-
if (newTags.length > 0) {
|
|
216
|
-
updatedTask.tags = [...updatedTask.tags, ...newTags];
|
|
217
|
-
changes.push("tags");
|
|
218
|
-
}
|
|
239
|
+
parsedPriority = priorityResult.value;
|
|
219
240
|
}
|
|
241
|
+
const parsedTags = options.tag ? parseTagsArray(options.tag) : [];
|
|
220
242
|
if (options.dependsOn) {
|
|
221
243
|
// Validate all dependency refs
|
|
222
244
|
for (const depRef of options.dependsOn) {
|
|
@@ -236,34 +258,12 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
236
258
|
};
|
|
237
259
|
}
|
|
238
260
|
}
|
|
239
|
-
updatedTask.depends_on = options.dependsOn.map(normalizeRefInput);
|
|
240
|
-
changes.push("depends_on");
|
|
241
|
-
}
|
|
242
|
-
// AC: @spec-task-clear-deps ac-1, ac-2 - Clear all dependencies
|
|
243
|
-
if (options.clearDeps) {
|
|
244
|
-
if (foundTask.depends_on.length === 0) {
|
|
245
|
-
// AC: @spec-task-clear-deps ac-2 - No changes needed
|
|
246
|
-
return {
|
|
247
|
-
success: true,
|
|
248
|
-
message: "No changes: task has no dependencies to clear",
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
updatedTask.depends_on = [];
|
|
252
|
-
changes.push("depends_on");
|
|
253
|
-
// Add note documenting the change
|
|
254
|
-
// AC: @task-set ac-author
|
|
255
|
-
const note = createNote(`Dependencies cleared (was: ${foundTask.depends_on.join(", ")})`, getAuthor(ctx.config?.identity?.author));
|
|
256
|
-
updatedTask.notes = [...updatedTask.notes, note];
|
|
257
261
|
}
|
|
258
262
|
// AC: @task-automation-eligibility ac-5, ac-11, ac-12, ac-18
|
|
259
263
|
// Handle automation status changes
|
|
260
264
|
// Note: --no-automation sets options.automation to false, so check that first
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
delete updatedTask.automation;
|
|
264
|
-
changes.push("automation");
|
|
265
|
-
}
|
|
266
|
-
else if (options.automation !== undefined) {
|
|
265
|
+
let validatedAutomation;
|
|
266
|
+
if (options.automation !== undefined && options.automation !== false) {
|
|
267
267
|
const automationResult = validateEnumOption(options.automation, ["eligible", "needs_review", "manual_only"], "automation status");
|
|
268
268
|
if (!automationResult.ok) {
|
|
269
269
|
return { success: false, error: automationResult.error };
|
|
@@ -275,15 +275,119 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
275
275
|
error: "Setting automation to needs_review requires --reason flag explaining why",
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
278
|
+
validatedAutomation = automationResult.value;
|
|
279
|
+
}
|
|
280
|
+
let updatedTask = foundTask;
|
|
281
|
+
updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
282
|
+
const nextTask = { ...latestTask };
|
|
283
|
+
const mutationChanges = [];
|
|
284
|
+
if (options.title) {
|
|
285
|
+
nextTask.title = options.title;
|
|
286
|
+
mutationChanges.push("title");
|
|
287
|
+
}
|
|
288
|
+
if (options.description !== undefined) {
|
|
289
|
+
if (options.description === "null" || options.description.trim() === "") {
|
|
290
|
+
delete nextTask.description;
|
|
291
|
+
mutationChanges.push("description: cleared");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
nextTask.description = options.description;
|
|
295
|
+
mutationChanges.push("description");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (options.specRef !== undefined) {
|
|
299
|
+
if (options.specRef === "null") {
|
|
300
|
+
nextTask.spec_ref = null;
|
|
301
|
+
mutationChanges.push("spec_ref: cleared");
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
nextTask.spec_ref = normalizeRefInput(options.specRef);
|
|
305
|
+
mutationChanges.push("spec_ref");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (options.metaRef !== undefined) {
|
|
309
|
+
if (options.metaRef === "null") {
|
|
310
|
+
nextTask.meta_ref = null;
|
|
311
|
+
mutationChanges.push("meta_ref: cleared");
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
nextTask.meta_ref = normalizeRefInput(options.metaRef);
|
|
315
|
+
mutationChanges.push("meta_ref");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (options.planRef !== undefined) {
|
|
319
|
+
if (options.planRef === "null") {
|
|
320
|
+
nextTask.plan_ref = null;
|
|
321
|
+
mutationChanges.push("plan_ref: cleared");
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
nextTask.plan_ref = normalizeRefInput(options.planRef);
|
|
325
|
+
mutationChanges.push("plan_ref");
|
|
326
|
+
}
|
|
286
327
|
}
|
|
328
|
+
if (options.reviewUrl !== undefined) {
|
|
329
|
+
if (options.reviewUrl === "null") {
|
|
330
|
+
delete nextTask.review_url;
|
|
331
|
+
mutationChanges.push("review_url: cleared");
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
nextTask.review_url = options.reviewUrl;
|
|
335
|
+
mutationChanges.push("review_url");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (parsedPriority !== undefined) {
|
|
339
|
+
nextTask.priority = parsedPriority;
|
|
340
|
+
mutationChanges.push("priority");
|
|
341
|
+
}
|
|
342
|
+
if (options.slug && !nextTask.slugs.includes(options.slug)) {
|
|
343
|
+
nextTask.slugs = [...nextTask.slugs, options.slug];
|
|
344
|
+
mutationChanges.push("slug");
|
|
345
|
+
}
|
|
346
|
+
if (parsedTags.length > 0) {
|
|
347
|
+
const newTags = parsedTags.filter((tag) => !nextTask.tags.includes(tag));
|
|
348
|
+
if (newTags.length > 0) {
|
|
349
|
+
nextTask.tags = [...nextTask.tags, ...newTags];
|
|
350
|
+
mutationChanges.push("tags");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (options.dependsOn) {
|
|
354
|
+
nextTask.depends_on = options.dependsOn.map(normalizeRefInput);
|
|
355
|
+
mutationChanges.push("depends_on");
|
|
356
|
+
}
|
|
357
|
+
if (options.clearDeps) {
|
|
358
|
+
if (latestTask.depends_on.length === 0) {
|
|
359
|
+
// AC: @spec-task-clear-deps ac-2 - No changes needed
|
|
360
|
+
noChangesMessage = "No changes: task has no dependencies to clear";
|
|
361
|
+
return latestTask;
|
|
362
|
+
}
|
|
363
|
+
nextTask.depends_on = [];
|
|
364
|
+
mutationChanges.push("depends_on");
|
|
365
|
+
// AC: @task-set ac-author
|
|
366
|
+
const note = createNote(`Dependencies cleared (was: ${latestTask.depends_on.join(", ")})`, getAuthor(ctx.config?.identity?.author));
|
|
367
|
+
nextTask.notes = [...nextTask.notes, note];
|
|
368
|
+
}
|
|
369
|
+
if (options.automation === false) {
|
|
370
|
+
delete nextTask.automation;
|
|
371
|
+
mutationChanges.push("automation");
|
|
372
|
+
}
|
|
373
|
+
else if (validatedAutomation) {
|
|
374
|
+
nextTask.automation = validatedAutomation;
|
|
375
|
+
mutationChanges.push("automation");
|
|
376
|
+
if (options.reason) {
|
|
377
|
+
const note = createNote(`Automation status set to ${validatedAutomation}: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
378
|
+
nextTask.notes = [...nextTask.notes, note];
|
|
379
|
+
mutationChanges.push("note");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
changes.splice(0, changes.length, ...mutationChanges);
|
|
383
|
+
return nextTask;
|
|
384
|
+
});
|
|
385
|
+
if (noChangesMessage) {
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
message: noChangesMessage,
|
|
389
|
+
data: { task: updatedTask },
|
|
390
|
+
};
|
|
287
391
|
}
|
|
288
392
|
// AC: @spec-task-set-batch ac-4 - Warn on no changes, don't fail
|
|
289
393
|
if (changes.length === 0) {
|
|
@@ -293,7 +397,6 @@ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index,
|
|
|
293
397
|
data: { task: updatedTask },
|
|
294
398
|
};
|
|
295
399
|
}
|
|
296
|
-
await saveTask(ctx, updatedTask);
|
|
297
400
|
await commitIfShadow(ctx.shadow, "task-set", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(", "));
|
|
298
401
|
return {
|
|
299
402
|
success: true,
|
|
@@ -592,9 +695,11 @@ Examples:
|
|
|
592
695
|
.description("Update task fields")
|
|
593
696
|
.option("--refs <refs...>", "Update multiple tasks (AC: @spec-task-set-batch ac-1)")
|
|
594
697
|
.option("--title <title>", "Update task title")
|
|
698
|
+
.option("--description <description>", "Update task description (use 'null' to clear)")
|
|
595
699
|
.option("--spec-ref <ref>", "Link to spec item (use 'null' to clear)")
|
|
596
700
|
.option("--meta-ref <ref>", "Link to meta item (use 'null' to clear)")
|
|
597
701
|
.option("--plan-ref <ref>", "Link to plan (use 'null' to clear)")
|
|
702
|
+
.option("--review-url <url>", "Set review URL (use 'null' to clear)")
|
|
598
703
|
.option("--priority <n>", "Set priority (1-5 or P1-P5)")
|
|
599
704
|
.option("--slug <slug>", "Add a slug alias")
|
|
600
705
|
.option("--tag <tag...>", "Add tags")
|
|
@@ -607,6 +712,7 @@ Examples:
|
|
|
607
712
|
.addHelpText("after", `
|
|
608
713
|
Examples:
|
|
609
714
|
$ kspec task set @task-slug --priority 2
|
|
715
|
+
$ kspec task set @task-slug --description "Updated context"
|
|
610
716
|
$ kspec task set @task-slug --depends-on @dep1 @dep2
|
|
611
717
|
$ kspec task set @task-slug --tag cli urgent
|
|
612
718
|
$ kspec task set --refs @task1 @task2 --priority 3`)
|
|
@@ -771,20 +877,22 @@ Examples:
|
|
|
771
877
|
process.exit(EXIT_CODES.ERROR);
|
|
772
878
|
}
|
|
773
879
|
}
|
|
774
|
-
// Build updated task
|
|
775
|
-
const updatedTask = { ...foundTask, ...validatedPatch };
|
|
776
880
|
// Track changes for output
|
|
777
881
|
const changes = Object.keys(validatedPatch);
|
|
778
882
|
if (options.dryRun) {
|
|
883
|
+
const dryRunTask = { ...foundTask, ...validatedPatch };
|
|
779
884
|
info("Dry run - no changes will be written");
|
|
780
885
|
info(`Would update: ${changes.join(", ")}`);
|
|
781
|
-
output({ changes, updated:
|
|
886
|
+
output({ changes, updated: dryRunTask }, () => {
|
|
782
887
|
console.log(`\nChanges: ${changes.join(", ")}\n`);
|
|
783
|
-
return formatTaskDetails(
|
|
888
|
+
return formatTaskDetails(dryRunTask, index);
|
|
784
889
|
});
|
|
785
890
|
return;
|
|
786
891
|
}
|
|
787
|
-
await
|
|
892
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => ({
|
|
893
|
+
...latestTask,
|
|
894
|
+
...validatedPatch,
|
|
895
|
+
}));
|
|
788
896
|
await commitIfShadow(ctx.shadow, "task-patch", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(", "));
|
|
789
897
|
success(`Patched task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(", ")})`, { task: updatedTask });
|
|
790
898
|
}
|
|
@@ -830,30 +938,47 @@ Examples:
|
|
|
830
938
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
831
939
|
}
|
|
832
940
|
}
|
|
833
|
-
// AC: @task-budget-enforcement ac-block-start, ac-no-session, ac-no-budget
|
|
834
|
-
// Check budget before starting — only when session is set
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
941
|
+
// AC: @task-budget-enforcement ac-block-start, ac-no-session, ac-no-budget, ac-needs-work-no-increment
|
|
942
|
+
// Check budget before starting — only when session is set.
|
|
943
|
+
// Skip budget check for needs_work→in_progress (fix cycles): the task
|
|
944
|
+
// already consumed a budget slot when originally started; blocking it
|
|
945
|
+
// would prevent completing already-assigned work.
|
|
946
|
+
if (foundTask.status !== "needs_work") {
|
|
947
|
+
const budgetCheck = await checkBudget(ctx.specDir, sessionId || undefined);
|
|
948
|
+
if (!budgetCheck.allowed) {
|
|
949
|
+
error(budgetCheck.reason);
|
|
950
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
951
|
+
}
|
|
839
952
|
}
|
|
840
953
|
// Update status
|
|
841
954
|
// AC: @session-scoped-task-claiming ac-stamp, ac-no-env
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
status
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
955
|
+
let transitionFromStatus = foundTask.status;
|
|
956
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
957
|
+
transitionFromStatus = latestTask.status;
|
|
958
|
+
return {
|
|
959
|
+
...latestTask,
|
|
960
|
+
status: "in_progress",
|
|
961
|
+
started_at: new Date().toISOString(),
|
|
962
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
963
|
+
};
|
|
964
|
+
});
|
|
849
965
|
await commitIfShadow(ctx.shadow, "task-start", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
850
|
-
// AC: @task-budget-enforcement ac-increment, ac-resume-no-increment
|
|
851
|
-
// Increment budget counter after successful start
|
|
852
|
-
// Resume case
|
|
853
|
-
//
|
|
854
|
-
|
|
966
|
+
// AC: @task-budget-enforcement ac-increment, ac-resume-no-increment, ac-needs-work-no-increment
|
|
967
|
+
// Increment budget counter after successful start, but only for
|
|
968
|
+
// genuine pending→in_progress transitions. Resume case returns early
|
|
969
|
+
// above. needs_work→in_progress is a fix cycle (task already consumed
|
|
970
|
+
// a budget slot when originally started) — don't double-count it.
|
|
971
|
+
if (sessionId && transitionFromStatus === "pending") {
|
|
855
972
|
await incrementBudget(ctx.specDir, sessionId);
|
|
856
973
|
}
|
|
974
|
+
// AC: @daemon-agent-dispatch ac-2, ac-7 - Notify daemon of state change (fire-and-forget)
|
|
975
|
+
postDispatchEvent({
|
|
976
|
+
taskId: updatedTask._ulid,
|
|
977
|
+
taskRef: `@${updatedTask.slugs[0] || updatedTask._ulid}`,
|
|
978
|
+
fromStatus: transitionFromStatus,
|
|
979
|
+
toStatus: updatedTask.status,
|
|
980
|
+
projectPath: ctx.rootDir,
|
|
981
|
+
});
|
|
857
982
|
success(`Started task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
858
983
|
task: updatedTask,
|
|
859
984
|
});
|
|
@@ -940,89 +1065,82 @@ Examples:
|
|
|
940
1065
|
},
|
|
941
1066
|
executeOperation: async (foundTask, { ctx, tasks, items, index, options }) => {
|
|
942
1067
|
try {
|
|
943
|
-
// AC: @spec-completion-enforcement ac-6
|
|
944
|
-
if (foundTask.status === "completed") {
|
|
945
|
-
return {
|
|
946
|
-
success: false,
|
|
947
|
-
error: errors.status.completeAlreadyCompleted,
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
// AC: @task-commands ac-1 - Allow --force to bypass all state checks
|
|
951
1068
|
const forcingCompletion = options.force;
|
|
952
|
-
// AC: @spec-completion-enforcement ac-7 - Allow skip-review bypass
|
|
953
|
-
if (!options.skipReview && !forcingCompletion) {
|
|
954
|
-
// AC: @spec-completion-enforcement ac-2
|
|
955
|
-
if (foundTask.status === "in_progress") {
|
|
956
|
-
return {
|
|
957
|
-
success: false,
|
|
958
|
-
error: errors.status.completeRequiresReview,
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
// AC: @spec-completion-enforcement ac-3
|
|
962
|
-
if (foundTask.status === "pending") {
|
|
963
|
-
return {
|
|
964
|
-
success: false,
|
|
965
|
-
error: errors.status.completeRequiresStart,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
// AC: @spec-completion-enforcement ac-4
|
|
969
|
-
if (foundTask.status === "blocked") {
|
|
970
|
-
return {
|
|
971
|
-
success: false,
|
|
972
|
-
error: errors.status.completeBlockedTask,
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
// AC: @spec-completion-enforcement ac-5
|
|
976
|
-
if (foundTask.status === "cancelled") {
|
|
977
|
-
return {
|
|
978
|
-
success: false,
|
|
979
|
-
error: errors.status.completeCancelledTask,
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
// AC: @spec-completion-enforcement ac-1 - Only pending_review allowed
|
|
983
|
-
if (foundTask.status !== "pending_review") {
|
|
984
|
-
return {
|
|
985
|
-
success: false,
|
|
986
|
-
error: errors.status.cannotComplete(foundTask.status),
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
1069
|
const now = new Date().toISOString();
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
let taskNotes = foundTask.notes;
|
|
994
|
-
if (options.skipReview && options.reason) {
|
|
995
|
-
const skipNote = createNote(`Completed with --skip-review: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
996
|
-
taskNotes = [...taskNotes, skipNote];
|
|
997
|
-
}
|
|
998
|
-
// AC: @task-commands ac-1 - Document force completion from non-standard state
|
|
999
|
-
const forcedFromNonStandard = forcingCompletion &&
|
|
1000
|
-
foundTask.status !== "pending_review";
|
|
1070
|
+
let transitionFromStatus = foundTask.status;
|
|
1071
|
+
let forcedFromNonStandard = false;
|
|
1001
1072
|
let forceStateDetail;
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1073
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1074
|
+
transitionFromStatus = latestTask.status;
|
|
1075
|
+
// AC: @spec-completion-enforcement ac-6
|
|
1076
|
+
if (latestTask.status === "completed") {
|
|
1077
|
+
throw new Error(errors.status.completeAlreadyCompleted);
|
|
1007
1078
|
}
|
|
1008
|
-
|
|
1009
|
-
if (options.
|
|
1010
|
-
|
|
1079
|
+
// AC: @spec-completion-enforcement ac-7 - Allow skip-review bypass
|
|
1080
|
+
if (!options.skipReview && !forcingCompletion) {
|
|
1081
|
+
// AC: @spec-completion-enforcement ac-2
|
|
1082
|
+
if (latestTask.status === "in_progress") {
|
|
1083
|
+
throw new Error(errors.status.completeRequiresReview);
|
|
1084
|
+
}
|
|
1085
|
+
// AC: @spec-completion-enforcement ac-3
|
|
1086
|
+
if (latestTask.status === "pending") {
|
|
1087
|
+
throw new Error(errors.status.completeRequiresStart);
|
|
1088
|
+
}
|
|
1089
|
+
// AC: @spec-completion-enforcement ac-4
|
|
1090
|
+
if (latestTask.status === "blocked") {
|
|
1091
|
+
throw new Error(errors.status.completeBlockedTask);
|
|
1092
|
+
}
|
|
1093
|
+
// AC: @spec-completion-enforcement ac-5
|
|
1094
|
+
if (latestTask.status === "cancelled") {
|
|
1095
|
+
throw new Error(errors.status.completeCancelledTask);
|
|
1096
|
+
}
|
|
1097
|
+
// AC: @spec-completion-enforcement ac-1 - Only pending_review allowed
|
|
1098
|
+
if (latestTask.status !== "pending_review") {
|
|
1099
|
+
throw new Error(errors.status.cannotComplete(latestTask.status));
|
|
1100
|
+
}
|
|
1011
1101
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1102
|
+
// AC: @spec-completion-enforcement ac-7 - Document skip-review reason
|
|
1103
|
+
// AC: @spec-completion-enforcement ac-author
|
|
1104
|
+
let taskNotes = latestTask.notes;
|
|
1105
|
+
if (options.skipReview && options.reason) {
|
|
1106
|
+
const skipNote = createNote(`Completed with --skip-review: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
1107
|
+
taskNotes = [...taskNotes, skipNote];
|
|
1108
|
+
}
|
|
1109
|
+
// AC: @task-commands ac-1 - Document force completion from non-standard state
|
|
1110
|
+
forcedFromNonStandard =
|
|
1111
|
+
forcingCompletion &&
|
|
1112
|
+
latestTask.status !== "pending_review";
|
|
1113
|
+
if (forcedFromNonStandard) {
|
|
1114
|
+
forceStateDetail = `from ${latestTask.status} state`;
|
|
1115
|
+
if (latestTask.status === "blocked") {
|
|
1116
|
+
const blockedBy = latestTask.blocked_by.join("; ");
|
|
1117
|
+
forceStateDetail += `. Was blocked by: ${blockedBy || "(dependency-blocked)"}`;
|
|
1118
|
+
}
|
|
1119
|
+
let forceMessage = `Completed with --force ${forceStateDetail}`;
|
|
1120
|
+
if (options.reason) {
|
|
1121
|
+
forceMessage += `. Reason: ${options.reason}`;
|
|
1122
|
+
}
|
|
1123
|
+
const forceNote = createNote(forceMessage, getAuthor(ctx.config?.identity?.author));
|
|
1124
|
+
taskNotes = [...taskNotes, forceNote];
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
...latestTask,
|
|
1128
|
+
status: "completed",
|
|
1129
|
+
completed_at: now,
|
|
1130
|
+
closed_reason: options.reason || null,
|
|
1131
|
+
started_at: latestTask.started_at || now,
|
|
1132
|
+
notes: taskNotes,
|
|
1133
|
+
};
|
|
1134
|
+
});
|
|
1025
1135
|
await commitIfShadow(ctx.shadow, "task-complete", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), options.reason);
|
|
1136
|
+
// AC: @daemon-agent-dispatch ac-2, ac-7 - Notify daemon of state change (fire-and-forget)
|
|
1137
|
+
postDispatchEvent({
|
|
1138
|
+
taskId: updatedTask._ulid,
|
|
1139
|
+
taskRef: `@${updatedTask.slugs[0] || updatedTask._ulid}`,
|
|
1140
|
+
fromStatus: transitionFromStatus,
|
|
1141
|
+
toStatus: updatedTask.status,
|
|
1142
|
+
projectPath: ctx.rootDir,
|
|
1143
|
+
});
|
|
1026
1144
|
// Sync spec implementation status (unless --no-sync)
|
|
1027
1145
|
if (options.sync !== false && foundTask.spec_ref) {
|
|
1028
1146
|
const updatedTasks = tasks.map((t) => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
|
|
@@ -1089,26 +1207,54 @@ Examples:
|
|
|
1089
1207
|
});
|
|
1090
1208
|
// kspec task submit <ref>
|
|
1091
1209
|
// Transitions in_progress → pending_review (code done, awaiting merge)
|
|
1210
|
+
// AC: @task-submit ac-submit-1, ac-submit-2, ac-submit-3
|
|
1092
1211
|
markMutating(task.command("submit <ref>"))
|
|
1093
1212
|
.description("Submit task for review (transitions to pending_review)")
|
|
1094
|
-
.
|
|
1213
|
+
.option("--review-url <url>", "PR or review URL")
|
|
1214
|
+
.action(async (ref, options) => {
|
|
1095
1215
|
try {
|
|
1216
|
+
// AC: @task-submit ac-submit-3 - Validate URL before any state change
|
|
1217
|
+
if (options.reviewUrl !== undefined) {
|
|
1218
|
+
try {
|
|
1219
|
+
new URL(options.reviewUrl);
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
error(`Invalid review URL: ${options.reviewUrl}`);
|
|
1223
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1096
1226
|
const ctx = await initContext();
|
|
1097
1227
|
const tasks = await loadAllTasks(ctx);
|
|
1098
1228
|
const items = await loadAllItems(ctx);
|
|
1099
1229
|
const index = new ReferenceIndex(tasks, items);
|
|
1100
1230
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1101
|
-
|
|
1102
|
-
|
|
1231
|
+
let transitionFromStatus = foundTask.status;
|
|
1232
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1233
|
+
transitionFromStatus = latestTask.status;
|
|
1234
|
+
if (latestTask.status !== "in_progress") {
|
|
1235
|
+
return latestTask;
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
...latestTask,
|
|
1239
|
+
status: "pending_review",
|
|
1240
|
+
submitted_at: new Date().toISOString(),
|
|
1241
|
+
// AC: @task-submit ac-submit-2
|
|
1242
|
+
...(options.reviewUrl !== undefined && { review_url: options.reviewUrl }),
|
|
1243
|
+
};
|
|
1244
|
+
});
|
|
1245
|
+
if (transitionFromStatus !== "in_progress") {
|
|
1246
|
+
error(`Cannot submit task with status: ${transitionFromStatus}. Task must be in_progress.`);
|
|
1103
1247
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
1104
1248
|
}
|
|
1105
|
-
const updatedTask = {
|
|
1106
|
-
...foundTask,
|
|
1107
|
-
status: "pending_review",
|
|
1108
|
-
submitted_at: new Date().toISOString(),
|
|
1109
|
-
};
|
|
1110
|
-
await saveTask(ctx, updatedTask);
|
|
1111
1249
|
await commitIfShadow(ctx.shadow, "task-submit", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1250
|
+
// AC: @daemon-agent-dispatch ac-2, ac-7 - Notify daemon of state change (fire-and-forget)
|
|
1251
|
+
postDispatchEvent({
|
|
1252
|
+
taskId: updatedTask._ulid,
|
|
1253
|
+
taskRef: `@${updatedTask.slugs[0] || updatedTask._ulid}`,
|
|
1254
|
+
fromStatus: transitionFromStatus,
|
|
1255
|
+
toStatus: updatedTask.status,
|
|
1256
|
+
projectPath: ctx.rootDir,
|
|
1257
|
+
});
|
|
1112
1258
|
success(`Submitted task for review: ${index.shortUlid(updatedTask._ulid)}`, { task: updatedTask });
|
|
1113
1259
|
}
|
|
1114
1260
|
catch (err) {
|
|
@@ -1128,23 +1274,38 @@ Examples:
|
|
|
1128
1274
|
const items = await loadAllItems(ctx);
|
|
1129
1275
|
const index = new ReferenceIndex(tasks, items);
|
|
1130
1276
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1131
|
-
|
|
1132
|
-
|
|
1277
|
+
let transitionFromStatus = foundTask.status;
|
|
1278
|
+
let cycleNumber = 0;
|
|
1279
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1280
|
+
transitionFromStatus = latestTask.status;
|
|
1281
|
+
if (latestTask.status !== "pending_review") {
|
|
1282
|
+
return latestTask;
|
|
1283
|
+
}
|
|
1284
|
+
// Track fix cycle count from existing kickback notes
|
|
1285
|
+
const existingKickbacks = latestTask.notes.filter((note) => note.content.includes("[FIX_CYCLE:")).length;
|
|
1286
|
+
cycleNumber = existingKickbacks + 1;
|
|
1287
|
+
const note = createNote(`[FIX_CYCLE: ${cycleNumber}] Review findings: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
1288
|
+
// AC: @session-scoped-task-claiming ac-claim-clear
|
|
1289
|
+
return {
|
|
1290
|
+
...latestTask,
|
|
1291
|
+
status: "needs_work",
|
|
1292
|
+
session_id: null,
|
|
1293
|
+
notes: [...latestTask.notes, note],
|
|
1294
|
+
};
|
|
1295
|
+
});
|
|
1296
|
+
if (transitionFromStatus !== "pending_review") {
|
|
1297
|
+
error(errors.status.cannotNeedsWork(transitionFromStatus));
|
|
1133
1298
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
1134
1299
|
}
|
|
1135
|
-
// Track fix cycle count from existing kickback notes
|
|
1136
|
-
const existingKickbacks = foundTask.notes.filter((n) => n.content.includes("[FIX_CYCLE:")).length;
|
|
1137
|
-
const cycleNumber = existingKickbacks + 1;
|
|
1138
|
-
const note = createNote(`[FIX_CYCLE: ${cycleNumber}] Review findings: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
|
|
1139
|
-
// AC: @session-scoped-task-claiming ac-claim-clear
|
|
1140
|
-
const updatedTask = {
|
|
1141
|
-
...foundTask,
|
|
1142
|
-
status: "needs_work",
|
|
1143
|
-
session_id: null,
|
|
1144
|
-
notes: [...foundTask.notes, note],
|
|
1145
|
-
};
|
|
1146
|
-
await saveTask(ctx, updatedTask);
|
|
1147
1300
|
await commitIfShadow(ctx.shadow, "task-needs-work", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `cycle ${cycleNumber}`);
|
|
1301
|
+
// AC: @daemon-agent-dispatch ac-2, ac-7 - Notify daemon of state change (fire-and-forget)
|
|
1302
|
+
postDispatchEvent({
|
|
1303
|
+
taskId: updatedTask._ulid,
|
|
1304
|
+
taskRef: `@${updatedTask.slugs[0] || updatedTask._ulid}`,
|
|
1305
|
+
fromStatus: transitionFromStatus,
|
|
1306
|
+
toStatus: updatedTask.status,
|
|
1307
|
+
projectPath: ctx.rootDir,
|
|
1308
|
+
});
|
|
1148
1309
|
success(`Kicked back task: ${index.shortUlid(updatedTask._ulid)} (fix cycle ${cycleNumber})`, { task: updatedTask });
|
|
1149
1310
|
}
|
|
1150
1311
|
catch (err) {
|
|
@@ -1163,18 +1324,33 @@ Examples:
|
|
|
1163
1324
|
const items = await loadAllItems(ctx);
|
|
1164
1325
|
const index = new ReferenceIndex(tasks, items);
|
|
1165
1326
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1327
|
+
let transitionFromStatus = foundTask.status;
|
|
1328
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1329
|
+
transitionFromStatus = latestTask.status;
|
|
1330
|
+
if (latestTask.status === "completed" ||
|
|
1331
|
+
latestTask.status === "cancelled") {
|
|
1332
|
+
return latestTask;
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
...latestTask,
|
|
1336
|
+
status: "blocked",
|
|
1337
|
+
blocked_by: [...latestTask.blocked_by, options.reason],
|
|
1338
|
+
};
|
|
1339
|
+
});
|
|
1340
|
+
if (transitionFromStatus === "completed" ||
|
|
1341
|
+
transitionFromStatus === "cancelled") {
|
|
1342
|
+
error(errors.status.cannotBlock(transitionFromStatus));
|
|
1169
1343
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
1170
1344
|
}
|
|
1171
|
-
const updatedTask = {
|
|
1172
|
-
...foundTask,
|
|
1173
|
-
status: "blocked",
|
|
1174
|
-
blocked_by: [...foundTask.blocked_by, options.reason],
|
|
1175
|
-
};
|
|
1176
|
-
await saveTask(ctx, updatedTask);
|
|
1177
1345
|
await commitIfShadow(ctx.shadow, "task-block", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1346
|
+
// AC: @daemon-agent-dispatch ac-2, ac-7 - Notify daemon of state change (fire-and-forget)
|
|
1347
|
+
postDispatchEvent({
|
|
1348
|
+
taskId: updatedTask._ulid,
|
|
1349
|
+
taskRef: `@${updatedTask.slugs[0] || updatedTask._ulid}`,
|
|
1350
|
+
fromStatus: transitionFromStatus,
|
|
1351
|
+
toStatus: updatedTask.status,
|
|
1352
|
+
projectPath: ctx.rootDir,
|
|
1353
|
+
});
|
|
1178
1354
|
success(`Blocked task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1179
1355
|
task: updatedTask,
|
|
1180
1356
|
});
|
|
@@ -1194,19 +1370,33 @@ Examples:
|
|
|
1194
1370
|
const items = await loadAllItems(ctx);
|
|
1195
1371
|
const index = new ReferenceIndex(tasks, items);
|
|
1196
1372
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1197
|
-
|
|
1373
|
+
let transitionFromStatus = foundTask.status;
|
|
1374
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1375
|
+
transitionFromStatus = latestTask.status;
|
|
1376
|
+
if (latestTask.status !== "blocked") {
|
|
1377
|
+
return latestTask;
|
|
1378
|
+
}
|
|
1379
|
+
// AC: @session-scoped-task-claiming ac-claim-clear
|
|
1380
|
+
return {
|
|
1381
|
+
...latestTask,
|
|
1382
|
+
status: "pending",
|
|
1383
|
+
blocked_by: [],
|
|
1384
|
+
session_id: null,
|
|
1385
|
+
};
|
|
1386
|
+
});
|
|
1387
|
+
if (transitionFromStatus !== "blocked") {
|
|
1198
1388
|
warn("Task is not blocked");
|
|
1199
1389
|
return;
|
|
1200
1390
|
}
|
|
1201
|
-
// AC: @session-scoped-task-claiming ac-claim-clear
|
|
1202
|
-
const updatedTask = {
|
|
1203
|
-
...foundTask,
|
|
1204
|
-
status: "pending",
|
|
1205
|
-
blocked_by: [],
|
|
1206
|
-
session_id: null,
|
|
1207
|
-
};
|
|
1208
|
-
await saveTask(ctx, updatedTask);
|
|
1209
1391
|
await commitIfShadow(ctx.shadow, "task-unblock", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1392
|
+
// AC: @daemon-agent-dispatch ac-2, ac-7 - Notify daemon of state change (fire-and-forget)
|
|
1393
|
+
postDispatchEvent({
|
|
1394
|
+
taskId: updatedTask._ulid,
|
|
1395
|
+
taskRef: `@${updatedTask.slugs[0] || updatedTask._ulid}`,
|
|
1396
|
+
fromStatus: transitionFromStatus,
|
|
1397
|
+
toStatus: updatedTask.status,
|
|
1398
|
+
projectPath: ctx.rootDir,
|
|
1399
|
+
});
|
|
1210
1400
|
success(`Unblocked task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1211
1401
|
task: updatedTask,
|
|
1212
1402
|
});
|
|
@@ -1291,54 +1481,60 @@ Examples:
|
|
|
1291
1481
|
const items = await loadAllItems(ctx);
|
|
1292
1482
|
const index = new ReferenceIndex(tasks, items);
|
|
1293
1483
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1294
|
-
|
|
1295
|
-
|
|
1484
|
+
let previousStatus = foundTask.status;
|
|
1485
|
+
const clearedFields = [];
|
|
1486
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1487
|
+
previousStatus = latestTask.status;
|
|
1488
|
+
// AC: @spec-task-reset ac-2 - Error if already pending
|
|
1489
|
+
if (latestTask.status === "pending") {
|
|
1490
|
+
return latestTask;
|
|
1491
|
+
}
|
|
1492
|
+
// AC: @spec-task-reset ac-1 - Reset to pending, clear completion-related fields
|
|
1493
|
+
const nextTask = {
|
|
1494
|
+
...latestTask,
|
|
1495
|
+
status: "pending",
|
|
1496
|
+
};
|
|
1497
|
+
clearedFields.splice(0, clearedFields.length);
|
|
1498
|
+
// Clear timestamps and reasons based on previous status
|
|
1499
|
+
if (latestTask.completed_at !== undefined &&
|
|
1500
|
+
latestTask.completed_at !== null) {
|
|
1501
|
+
nextTask.completed_at = null;
|
|
1502
|
+
clearedFields.push("completed_at");
|
|
1503
|
+
}
|
|
1504
|
+
if (latestTask.started_at !== undefined &&
|
|
1505
|
+
latestTask.started_at !== null) {
|
|
1506
|
+
nextTask.started_at = null;
|
|
1507
|
+
clearedFields.push("started_at");
|
|
1508
|
+
}
|
|
1509
|
+
if (latestTask.closed_reason !== undefined &&
|
|
1510
|
+
latestTask.closed_reason !== null) {
|
|
1511
|
+
nextTask.closed_reason = null;
|
|
1512
|
+
clearedFields.push("closed_reason");
|
|
1513
|
+
}
|
|
1514
|
+
if (latestTask.blocked_by.length > 0) {
|
|
1515
|
+
nextTask.blocked_by = [];
|
|
1516
|
+
clearedFields.push("blocked_by");
|
|
1517
|
+
}
|
|
1518
|
+
// AC: @session-scoped-task-claiming ac-claim-clear
|
|
1519
|
+
if (latestTask.session_id) {
|
|
1520
|
+
nextTask.session_id = null;
|
|
1521
|
+
clearedFields.push("session_id");
|
|
1522
|
+
}
|
|
1523
|
+
// AC: @spec-task-reset ac-4 - Add note documenting the reset
|
|
1524
|
+
// AC: @spec-task-reset ac-author
|
|
1525
|
+
const hadCancelReason = latestTask.closed_reason && latestTask.status === "cancelled";
|
|
1526
|
+
const cancelReasonText = hadCancelReason
|
|
1527
|
+
? ` (was cancelled: ${latestTask.closed_reason})`
|
|
1528
|
+
: "";
|
|
1529
|
+
const noteContent = `Reset from ${latestTask.status} to pending${cancelReasonText}`;
|
|
1530
|
+
const note = createNote(noteContent, getAuthor(ctx.config?.identity?.author));
|
|
1531
|
+
nextTask.notes = [...nextTask.notes, note];
|
|
1532
|
+
return nextTask;
|
|
1533
|
+
});
|
|
1534
|
+
if (previousStatus === "pending") {
|
|
1296
1535
|
error("Task is already pending");
|
|
1297
1536
|
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
1298
1537
|
}
|
|
1299
|
-
// Track previous status and reason for note (AC-4)
|
|
1300
|
-
const previousStatus = foundTask.status;
|
|
1301
|
-
const hadCancelReason = foundTask.closed_reason && foundTask.status === "cancelled";
|
|
1302
|
-
const cancelReasonText = hadCancelReason
|
|
1303
|
-
? ` (was cancelled: ${foundTask.closed_reason})`
|
|
1304
|
-
: "";
|
|
1305
|
-
// AC: @spec-task-reset ac-1 - Reset to pending, clear completion-related fields
|
|
1306
|
-
const clearedFields = [];
|
|
1307
|
-
const updatedTask = {
|
|
1308
|
-
...foundTask,
|
|
1309
|
-
status: "pending",
|
|
1310
|
-
};
|
|
1311
|
-
// Clear timestamps and reasons based on previous status
|
|
1312
|
-
if (foundTask.completed_at !== undefined &&
|
|
1313
|
-
foundTask.completed_at !== null) {
|
|
1314
|
-
updatedTask.completed_at = null;
|
|
1315
|
-
clearedFields.push("completed_at");
|
|
1316
|
-
}
|
|
1317
|
-
if (foundTask.started_at !== undefined &&
|
|
1318
|
-
foundTask.started_at !== null) {
|
|
1319
|
-
updatedTask.started_at = null;
|
|
1320
|
-
clearedFields.push("started_at");
|
|
1321
|
-
}
|
|
1322
|
-
if (foundTask.closed_reason !== undefined &&
|
|
1323
|
-
foundTask.closed_reason !== null) {
|
|
1324
|
-
updatedTask.closed_reason = null;
|
|
1325
|
-
clearedFields.push("closed_reason");
|
|
1326
|
-
}
|
|
1327
|
-
if (foundTask.blocked_by.length > 0) {
|
|
1328
|
-
updatedTask.blocked_by = [];
|
|
1329
|
-
clearedFields.push("blocked_by");
|
|
1330
|
-
}
|
|
1331
|
-
// AC: @session-scoped-task-claiming ac-claim-clear
|
|
1332
|
-
if (foundTask.session_id) {
|
|
1333
|
-
updatedTask.session_id = null;
|
|
1334
|
-
clearedFields.push("session_id");
|
|
1335
|
-
}
|
|
1336
|
-
// AC: @spec-task-reset ac-4 - Add note documenting the reset
|
|
1337
|
-
// AC: @spec-task-reset ac-author
|
|
1338
|
-
const noteContent = `Reset from ${previousStatus} to pending${cancelReasonText}`;
|
|
1339
|
-
const note = createNote(noteContent, getAuthor(ctx.config?.identity?.author));
|
|
1340
|
-
updatedTask.notes = [...updatedTask.notes, note];
|
|
1341
|
-
await saveTask(ctx, updatedTask);
|
|
1342
1538
|
// AC: @spec-task-reset ac-3 - Shadow commit with message task-reset
|
|
1343
1539
|
await commitIfShadow(ctx.shadow, "task-reset", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `from ${previousStatus}`);
|
|
1344
1540
|
// AC: @spec-task-reset ac-6 - JSON output includes previous_status, new_status, cleared_fields
|
|
@@ -1459,11 +1655,10 @@ Examples:
|
|
|
1459
1655
|
const index = new ReferenceIndex(tasks, items);
|
|
1460
1656
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1461
1657
|
const note = createNote(message, options.author, options.supersedes);
|
|
1462
|
-
const updatedTask = {
|
|
1463
|
-
...
|
|
1464
|
-
notes: [...
|
|
1465
|
-
};
|
|
1466
|
-
await saveTask(ctx, updatedTask);
|
|
1658
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => ({
|
|
1659
|
+
...latestTask,
|
|
1660
|
+
notes: [...latestTask.notes, note],
|
|
1661
|
+
}));
|
|
1467
1662
|
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1468
1663
|
success(`Added note to task: ${index.shortUlid(updatedTask._ulid)}`, {
|
|
1469
1664
|
note,
|
|
@@ -1561,8 +1756,11 @@ Examples:
|
|
|
1561
1756
|
possibleRefs.push(`@${specItem.slugs[0]} ${ac.id}`);
|
|
1562
1757
|
possibleRefs.push(`@${specItem.slugs[0]}`);
|
|
1563
1758
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1759
|
+
for (let prefixLength = 8; prefixLength <= specItem._ulid.length; prefixLength++) {
|
|
1760
|
+
const prefix = specItem._ulid.slice(0, prefixLength);
|
|
1761
|
+
possibleRefs.push(`@${prefix} ${ac.id}`);
|
|
1762
|
+
possibleRefs.push(`@${prefix}`);
|
|
1763
|
+
}
|
|
1566
1764
|
const isCovered = possibleRefs.some((ref) => coveredACs.has(ref));
|
|
1567
1765
|
if (isCovered) {
|
|
1568
1766
|
covered.push(ac.id);
|
|
@@ -1701,16 +1899,18 @@ Examples:
|
|
|
1701
1899
|
const items = await loadAllItems(ctx);
|
|
1702
1900
|
const index = new ReferenceIndex(tasks, items);
|
|
1703
1901
|
const foundTask = resolveTaskRef(ref, tasks, index);
|
|
1704
|
-
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1902
|
+
let todo = createTodo(1, text, options.author);
|
|
1903
|
+
const updatedTask = await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1904
|
+
// Calculate next ID (max existing + 1, or 1 if none)
|
|
1905
|
+
const nextId = latestTask.todos.length > 0
|
|
1906
|
+
? Math.max(...latestTask.todos.map((entry) => entry.id)) + 1
|
|
1907
|
+
: 1;
|
|
1908
|
+
todo = createTodo(nextId, text, options.author);
|
|
1909
|
+
return {
|
|
1910
|
+
...latestTask,
|
|
1911
|
+
todos: [...latestTask.todos, todo],
|
|
1912
|
+
};
|
|
1913
|
+
});
|
|
1714
1914
|
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1715
1915
|
success(`Added todo #${todo.id} to task: ${index.shortUlid(updatedTask._ulid)}`, { todo });
|
|
1716
1916
|
}
|
|
@@ -1734,29 +1934,42 @@ Examples:
|
|
|
1734
1934
|
error(errors.todo.invalidId(idStr));
|
|
1735
1935
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1736
1936
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1937
|
+
let todoState;
|
|
1938
|
+
let updatedTodo;
|
|
1939
|
+
await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1940
|
+
const todoIndex = latestTask.todos.findIndex((todo) => todo.id === id);
|
|
1941
|
+
if (todoIndex === -1) {
|
|
1942
|
+
todoState = "not_found";
|
|
1943
|
+
return latestTask;
|
|
1944
|
+
}
|
|
1945
|
+
if (latestTask.todos[todoIndex].done) {
|
|
1946
|
+
todoState = "already_done";
|
|
1947
|
+
updatedTodo = latestTask.todos[todoIndex];
|
|
1948
|
+
return latestTask;
|
|
1949
|
+
}
|
|
1950
|
+
const updatedTodos = [...latestTask.todos];
|
|
1951
|
+
updatedTodos[todoIndex] = {
|
|
1952
|
+
...updatedTodos[todoIndex],
|
|
1953
|
+
done: true,
|
|
1954
|
+
done_at: new Date().toISOString(),
|
|
1955
|
+
};
|
|
1956
|
+
updatedTodo = updatedTodos[todoIndex];
|
|
1957
|
+
return {
|
|
1958
|
+
...latestTask,
|
|
1959
|
+
todos: updatedTodos,
|
|
1960
|
+
};
|
|
1961
|
+
});
|
|
1962
|
+
if (todoState === "not_found") {
|
|
1739
1963
|
error(errors.todo.notFound(id));
|
|
1740
1964
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
1741
1965
|
}
|
|
1742
|
-
if (
|
|
1966
|
+
if (todoState === "already_done") {
|
|
1743
1967
|
warn(`Todo #${id} is already done`);
|
|
1744
1968
|
return;
|
|
1745
1969
|
}
|
|
1746
|
-
const updatedTodos = [...foundTask.todos];
|
|
1747
|
-
updatedTodos[todoIndex] = {
|
|
1748
|
-
...updatedTodos[todoIndex],
|
|
1749
|
-
done: true,
|
|
1750
|
-
done_at: new Date().toISOString(),
|
|
1751
|
-
};
|
|
1752
|
-
const updatedTask = {
|
|
1753
|
-
...foundTask,
|
|
1754
|
-
todos: updatedTodos,
|
|
1755
|
-
};
|
|
1756
|
-
await saveTask(ctx, updatedTask);
|
|
1757
1970
|
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1758
1971
|
success(`Marked todo #${id} as done`, {
|
|
1759
|
-
todo:
|
|
1972
|
+
todo: updatedTodo,
|
|
1760
1973
|
});
|
|
1761
1974
|
}
|
|
1762
1975
|
catch (err) {
|
|
@@ -1779,29 +1992,42 @@ Examples:
|
|
|
1779
1992
|
error(errors.todo.invalidId(idStr));
|
|
1780
1993
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1781
1994
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1995
|
+
let todoState;
|
|
1996
|
+
let updatedTodo;
|
|
1997
|
+
await mutateTaskAtomically(ctx, foundTask, (latestTask) => {
|
|
1998
|
+
const todoIndex = latestTask.todos.findIndex((todo) => todo.id === id);
|
|
1999
|
+
if (todoIndex === -1) {
|
|
2000
|
+
todoState = "not_found";
|
|
2001
|
+
return latestTask;
|
|
2002
|
+
}
|
|
2003
|
+
if (!latestTask.todos[todoIndex].done) {
|
|
2004
|
+
todoState = "already_not_done";
|
|
2005
|
+
updatedTodo = latestTask.todos[todoIndex];
|
|
2006
|
+
return latestTask;
|
|
2007
|
+
}
|
|
2008
|
+
const updatedTodos = [...latestTask.todos];
|
|
2009
|
+
updatedTodos[todoIndex] = {
|
|
2010
|
+
...updatedTodos[todoIndex],
|
|
2011
|
+
done: false,
|
|
2012
|
+
done_at: undefined,
|
|
2013
|
+
};
|
|
2014
|
+
updatedTodo = updatedTodos[todoIndex];
|
|
2015
|
+
return {
|
|
2016
|
+
...latestTask,
|
|
2017
|
+
todos: updatedTodos,
|
|
2018
|
+
};
|
|
2019
|
+
});
|
|
2020
|
+
if (todoState === "not_found") {
|
|
1784
2021
|
error(errors.todo.notFound(id));
|
|
1785
2022
|
process.exit(EXIT_CODES.NOT_FOUND);
|
|
1786
2023
|
}
|
|
1787
|
-
if (
|
|
2024
|
+
if (todoState === "already_not_done") {
|
|
1788
2025
|
warn(`Todo #${id} is not done`);
|
|
1789
2026
|
return;
|
|
1790
2027
|
}
|
|
1791
|
-
const updatedTodos = [...foundTask.todos];
|
|
1792
|
-
updatedTodos[todoIndex] = {
|
|
1793
|
-
...updatedTodos[todoIndex],
|
|
1794
|
-
done: false,
|
|
1795
|
-
done_at: undefined,
|
|
1796
|
-
};
|
|
1797
|
-
const updatedTask = {
|
|
1798
|
-
...foundTask,
|
|
1799
|
-
todos: updatedTodos,
|
|
1800
|
-
};
|
|
1801
|
-
await saveTask(ctx, updatedTask);
|
|
1802
2028
|
await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
|
|
1803
2029
|
success(`Marked todo #${id} as not done`, {
|
|
1804
|
-
todo:
|
|
2030
|
+
todo: updatedTodo,
|
|
1805
2031
|
});
|
|
1806
2032
|
}
|
|
1807
2033
|
catch (err) {
|