@wrongstack/tools 0.293.0 → 0.295.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/dist/_env.d.ts +1 -1
- package/dist/_env.d.ts.map +1 -1
- package/dist/_fetch-guard.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/_util.d.ts +1 -1
- package/dist/_util.d.ts.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +81 -16
- package/dist/audit.js.map +2 -2
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +66 -52
- package/dist/bash.js.map +3 -3
- package/dist/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +1 -1
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/browser/index.js +10 -1
- package/dist/browser/index.js.map +2 -2
- package/dist/browser/manager.d.ts +2 -0
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/tools.d.ts +1 -1
- package/dist/browser/tools.d.ts.map +1 -1
- package/dist/builtin.d.ts +1 -1
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +1394 -553
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/background-indexer.d.ts +2 -2
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +3 -1
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/generic-parser.d.ts +34 -0
- package/dist/codebase-index/generic-parser.d.ts.map +1 -0
- package/dist/codebase-index/go-parser.d.ts +1 -1
- package/dist/codebase-index/go-parser.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +4 -2
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +1036 -341
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/indexer.d.ts +6 -1
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/json-parser.d.ts +1 -1
- package/dist/codebase-index/languages.d.ts +25 -0
- package/dist/codebase-index/languages.d.ts.map +1 -0
- package/dist/codebase-index/py-parser.d.ts +9 -1
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.d.ts +1 -1
- package/dist/codebase-index/rs-parser.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +9 -2
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/spawn-gate.d.ts +15 -0
- package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
- package/dist/codebase-index/ts-parser.d.ts +2 -2
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +961 -317
- package/dist/codebase-index/worker.js.map +4 -4
- package/dist/codebase-index/writer.d.ts +83 -4
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/codebase-index/yaml-parser.d.ts +1 -1
- package/dist/design.d.ts +5 -2
- package/dist/design.d.ts.map +1 -1
- package/dist/design.js +36 -3
- package/dist/design.js.map +2 -2
- package/dist/diff.d.ts +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -2
- package/dist/diff.js.map +1 -1
- package/dist/document.d.ts +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +1 -1
- package/dist/document.js.map +1 -1
- package/dist/e2e.d.ts +1 -1
- package/dist/e2e.d.ts.map +1 -1
- package/dist/e2e.js +1 -1
- package/dist/e2e.js.map +1 -1
- package/dist/edit.d.ts +1 -1
- package/dist/edit.d.ts.map +1 -1
- package/dist/edit.js +3 -3
- package/dist/edit.js.map +2 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +60 -21
- package/dist/exec.js.map +3 -3
- package/dist/fetch.d.ts +1 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +4 -5
- package/dist/fetch.js.map +2 -2
- package/dist/format.d.ts +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +81 -16
- package/dist/format.js.map +2 -2
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +3 -2
- package/dist/git.js.map +2 -2
- package/dist/glob.d.ts +1 -1
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +2 -2
- package/dist/glob.js.map +1 -1
- package/dist/grep.d.ts +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +4 -3
- package/dist/grep.js.map +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1657 -705
- package/dist/index.js.map +4 -4
- package/dist/install.d.ts +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +85 -17
- package/dist/install.js.map +2 -2
- package/dist/json.d.ts +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -2
- package/dist/json.js.map +2 -2
- package/dist/kanban.d.ts +4 -3
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +94 -27
- package/dist/kanban.js.map +2 -2
- package/dist/languages/execute-tool.d.ts +1 -1
- package/dist/languages/execute-tool.d.ts.map +1 -1
- package/dist/languages/execute.d.ts +1 -1
- package/dist/languages/execute.d.ts.map +1 -1
- package/dist/languages/index.d.ts +1 -1
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +99 -17
- package/dist/languages/index.js.map +2 -2
- package/dist/languages/package-tool.d.ts +3 -4
- package/dist/languages/package-tool.d.ts.map +1 -1
- package/dist/languages/plan.d.ts.map +1 -1
- package/dist/languages/profiles/additional.d.ts.map +1 -1
- package/dist/languages/profiles/primary.d.ts.map +1 -1
- package/dist/languages/tool.d.ts +1 -1
- package/dist/languages/tool.d.ts.map +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +81 -16
- package/dist/lint.js.map +2 -2
- package/dist/logs.d.ts +1 -1
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +2 -2
- package/dist/logs.js.map +1 -1
- package/dist/memory.d.ts +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1 -1
- package/dist/memory.js.map +1 -1
- package/dist/mode.d.ts +1 -1
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/outdated.d.ts +1 -1
- package/dist/outdated.d.ts.map +1 -1
- package/dist/outdated.js +101 -19
- package/dist/outdated.js.map +2 -2
- package/dist/pack.d.ts +1 -1
- package/dist/pack.js +1394 -553
- package/dist/pack.js.map +4 -4
- package/dist/patch.d.ts +1 -1
- package/dist/patch.d.ts.map +1 -1
- package/dist/patch.js +2 -2
- package/dist/patch.js.map +1 -1
- package/dist/plan.d.ts +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +7 -7
- package/dist/plan.js.map +2 -2
- package/dist/process-guardian.d.ts +7 -0
- package/dist/process-guardian.d.ts.map +1 -1
- package/dist/process-registry-persistent.d.ts +5 -0
- package/dist/process-registry-persistent.d.ts.map +1 -1
- package/dist/process-registry.d.ts +9 -1
- package/dist/process-registry.d.ts.map +1 -1
- package/dist/process-registry.js +18 -3
- package/dist/process-registry.js.map +2 -2
- package/dist/ps-slash.js +54 -15
- package/dist/ps-slash.js.map +2 -2
- package/dist/read.d.ts +1 -1
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js +3 -2
- package/dist/read.js.map +2 -2
- package/dist/replace.d.ts +1 -1
- package/dist/replace.d.ts.map +1 -1
- package/dist/replace.js +4 -4
- package/dist/replace.js.map +2 -2
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +2 -2
- package/dist/scaffold.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +4 -4
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +4 -1
- package/dist/session-kanban.d.ts.map +1 -1
- package/dist/session-kanban.js +23 -15
- package/dist/session-kanban.js.map +2 -2
- package/dist/set-working-dir.d.ts +1 -1
- package/dist/set-working-dir.d.ts.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +2 -5
- package/dist/skill.js.map +2 -2
- package/dist/spawn-background.d.ts.map +1 -1
- package/dist/task.d.ts +3 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +6 -8
- package/dist/task.js.map +2 -2
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +81 -16
- package/dist/test.js.map +2 -2
- package/dist/todo.d.ts +2 -1
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +4 -4
- package/dist/todo.js.map +2 -2
- package/dist/tool-help.d.ts +1 -1
- package/dist/tool-help.d.ts.map +1 -1
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-search.d.ts +1 -1
- package/dist/tool-search.d.ts.map +1 -1
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-tier.d.ts +16 -0
- package/dist/tool-tier.d.ts.map +1 -0
- package/dist/tool-tier.js +22233 -0
- package/dist/tool-tier.js.map +7 -0
- package/dist/tool-use.d.ts +1 -1
- package/dist/tool-use.d.ts.map +1 -1
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.d.ts +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +2 -2
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.d.ts +1 -1
- package/dist/typecheck.d.ts.map +1 -1
- package/dist/typecheck.js +81 -16
- package/dist/typecheck.js.map +2 -2
- package/dist/write.d.ts +1 -1
- package/dist/write.d.ts.map +1 -1
- package/dist/write.js +3 -2
- package/dist/write.js.map +2 -2
- package/package.json +7 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set-working-dir.d.ts","sourceRoot":"","sources":["../src/set-working-dir.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"set-working-dir.d.ts","sourceRoot":"","sources":["../src/set-working-dir.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAKnD,UAAU,kBAAkB;IAC1B,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,UAAU,mBAAmB;IAC3B,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAID,eAAO,MAAM,iBAAiB,EAAE,IAAI,CAAC,kBAAkB,EAAE,mBAAmB,CAwE3E,CAAC"}
|
package/dist/skill.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SkillLoader, Tool } from '@wrongstack/core/types';
|
|
2
2
|
interface SkillToolInput {
|
|
3
3
|
name: string;
|
|
4
4
|
/** Optional relative path of a bundled resource to load (e.g. `references/REF.md`, `scripts/extract.py`, `assets/template.html`). Omit to list resources. */
|
package/dist/skill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGhE,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,6JAA6J;IAC7J,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,UAAU,aAAa;IACrB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CAC7C;AAOD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAiG7F"}
|
package/dist/skill.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// src/skill.ts
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
stripFrontmatter,
|
|
7
|
-
ToolValidationError
|
|
8
|
-
} from "@wrongstack/core";
|
|
4
|
+
import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
|
|
5
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
9
6
|
var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
|
|
10
7
|
var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
|
|
11
8
|
var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
|
package/dist/skill.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/skill.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Dirent } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {
|
|
5
|
-
"mappings": ";AACA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB
|
|
4
|
+
"sourcesContent": ["import type { Dirent } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { SKILL_LIMITS, stripFrontmatter } from '@wrongstack/core/skills';\nimport type { SkillLoader, Tool } from '@wrongstack/core/types';\nimport { ToolValidationError } from '@wrongstack/core/types';\n\ninterface SkillToolInput {\n name: string;\n /** Optional relative path of a bundled resource to load (e.g. `references/REF.md`, `scripts/extract.py`, `assets/template.html`). Omit to list resources. */\n resource?: string | undefined;\n}\n\ninterface SkillResource {\n /** Path relative to the skill directory (e.g. `scripts/extract.py`). */\n path: string;\n bytes: number;\n}\n\ninterface LoadedResource {\n rel: string;\n absPath: string;\n content: string;\n bytes: number;\n truncated: boolean;\n}\n\ninterface SkillToolOutput {\n name: string;\n description: string;\n /** Frontmatter-stripped SKILL.md body (capped). */\n body: string;\n /** All bundled resource files (recursive), when no specific resource was requested. */\n resources: SkillResource[];\n /** Absolute directory of the skill \u2014 run scripts via bash using paths under here. */\n dir: string;\n /** When `resource` was requested: the loaded file. */\n loadedResource?: LoadedResource | undefined;\n}\n\nconst MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;\nconst MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;\nconst MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);\n\n/**\n * Skill tool \u2014 the agentskills.io progressive-disclosure primitive.\n *\n * - `skill({ name })` \u2192 SKILL.md body + a recursive listing of every bundled\n * resource file (scripts/ references/ assets/ templates/ \u2026 any subdir).\n * - `skill({ name, resource })` \u2192 the content of that one resource file.\n *\n * Use this (not the `read` tool) to load skill resources: it works for foreign\n * skills that live outside the project root (e.g. `~/.claude/skills/\u2026`), which\n * a project-root-restricted `read` tool may refuse. Scripts are returned with\n * their absolute path so the agent can run them via `bash`.\n */\nexport function makeSkillTool(skillLoader: SkillLoader): Tool<SkillToolInput, SkillToolOutput> {\n return {\n name: 'skill',\n category: 'Skills',\n description:\n \"Load a skill's instructions or a bundled resource on demand (agentskills.io progressive disclosure). \" +\n 'With only `name`: returns the SKILL.md body + a list of bundled resource files. ' +\n 'With `name` + `resource` (a relative path like `references/REF.md` or `scripts/extract.py`): returns that file content. ' +\n 'Prefer this over the read tool for skill files \u2014 it reaches foreign skills outside the project root.',\n usageHint:\n 'Load a skill body or one of its bundled resources (progressive disclosure).\\n\\n' +\n 'WHEN TO USE:\\n' +\n '- A task matches a skill trigger \u2192 load the body: skill({ name })\\n' +\n '- You need a reference/template/script bundled with the skill \u2192 skill({ name, resource: \"references/REF.md\" })\\n\\n' +\n 'The body call lists every bundled resource; load the ones you need. Run scripts via bash using the returned abs path.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'document',\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'Exact skill name (as shown in the available-skills list).',\n },\n resource: {\n type: 'string',\n description:\n 'Optional relative path of a bundled resource to load (e.g. references/REF.md, scripts/extract.py, assets/template.html). Omit to list resources.',\n },\n },\n required: ['name'],\n },\n async execute(input, ctx) {\n const name = input?.name?.trim();\n if (!name) {\n throw new ToolValidationError({ message: 'skill: name is required', field: 'name' });\n }\n const manifest = await skillLoader.find(name);\n if (!manifest) {\n throw new ToolValidationError({\n message: `skill \"${name}\" not found \u2014 use /skill to list available skills`,\n field: 'name',\n });\n }\n const dir = path.dirname(manifest.path);\n\n // Loading a specific resource takes precedence; skip the (potentially large)\n // full listing in that case to keep the response focused.\n let loadedResource: LoadedResource | undefined;\n if (input.resource?.trim()) {\n loadedResource = await loadResource(dir, input.resource.trim());\n }\n\n const raw = await skillLoader.readBody(name);\n const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);\n const resources = loadedResource ? [] : await listResources(dir);\n\n try {\n await ctx?.session?.append({\n type: 'skill_activated',\n ts: new Date().toISOString(),\n skillName: manifest.name,\n });\n } catch {\n // best-effort: session recording must never break skill loading\n }\n\n return {\n name: manifest.name,\n description: manifest.description,\n body,\n resources,\n dir,\n loadedResource,\n };\n },\n serialize(output) {\n if (output.loadedResource) {\n const lr = output.loadedResource;\n const note = lr.truncated\n ? ` (truncated to ${lr.content.length} chars of ${lr.bytes} B)`\n : '';\n return `# Resource: ${output.name}/${lr.rel}\\n(abs path: ${lr.absPath})${note}\\n\\n${lr.content}`;\n }\n const head = `# Skill: ${output.name}\\n${output.description}\\n\\n${output.body}`;\n if (output.resources.length === 0) return head;\n const listing = output.resources.map((r) => `- ${r.path} (${r.bytes} B)`).join('\\n');\n return (\n `${head}\\n\\n## Bundled resources (load on demand)\\n` +\n `Load any with: \\`skill({ name: \"${output.name}\", resource: \"<path>\" })\\`. ` +\n `Run scripts via bash using their abs path under ${output.dir}.\\n${listing}`\n );\n },\n };\n}\n\n/** Load a single bundled resource by relative path, with a path-traversal guard. */\nasync function loadResource(skillDir: string, rel: string): Promise<LoadedResource> {\n const norm = rel.replace(/\\\\/g, '/');\n if (path.isAbsolute(rel) || norm.split('/').some((seg) => seg === '..')) {\n throw new ToolValidationError({\n message: `skill: invalid resource path \"${rel}\"`,\n field: 'resource',\n });\n }\n const absPath = path.resolve(skillDir, rel);\n const root = path.resolve(skillDir);\n if (absPath !== root && !absPath.startsWith(root + path.sep)) {\n throw new ToolValidationError({\n message: `skill: resource \"${rel}\" escapes the skill directory`,\n field: 'resource',\n });\n }\n let buf: Buffer;\n try {\n buf = await fs.readFile(absPath);\n } catch {\n throw new ToolValidationError({\n message: `skill: resource \"${rel}\" not readable`,\n field: 'resource',\n });\n }\n const raw = buf.toString('utf8');\n const truncated = raw.length > MAX_RESOURCE_CHARS;\n return {\n rel: norm,\n absPath,\n content: truncated ? raw.slice(0, MAX_RESOURCE_CHARS) : raw,\n bytes: buf.length,\n truncated,\n };\n}\n\n/** Recursively list every file in the skill dir (any subdir), except SKILL.md. */\nasync function listResources(skillDir: string): Promise<SkillResource[]> {\n const out: SkillResource[] = [];\n await walk(skillDir, skillDir, out);\n out.sort((a, b) => a.path.localeCompare(b.path));\n return out.slice(0, MAX_LISTED_RESOURCES);\n}\n\nasync function walk(root: string, dir: string, out: SkillResource[]): Promise<void> {\n if (out.length >= MAX_LISTED_RESOURCES) return;\n let entries: Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (out.length >= MAX_LISTED_RESOURCES) return;\n const fullPath = path.join(dir, e.name);\n let isDir = e.isDirectory();\n if (e.isSymbolicLink()) {\n try {\n isDir = (await fs.stat(fullPath)).isDirectory();\n } catch {\n continue; // broken symlink\n }\n }\n if (isDir) {\n if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue;\n await walk(root, fullPath, out);\n } else if (e.isFile()) {\n if (e.name === 'SKILL.md' || e.name === 'SKILL.save.md') continue;\n try {\n const stat = await fs.stat(fullPath);\n const rel = path.relative(root, fullPath).split(path.sep).join('/');\n out.push({ path: rel, bytes: stat.size });\n } catch {\n // skip unreadable entry\n }\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["stat"]
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-background.d.ts","sourceRoot":"","sources":["../src/spawn-background.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAU9D,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG;IAC7D,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;CACjC,
|
|
1
|
+
{"version":3,"file":"spawn-background.d.ts","sourceRoot":"","sources":["../src/spawn-background.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAU9D,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG;IAC7D,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;CACjC,CAqCA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAAM,EAAO,EACnB,GAAG,CAAC,EAAE,MAAM,EACZ,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B;IACD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;CACjC,CAsCA;AAGD,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/task.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type
|
|
1
|
+
import type { TaskStatus } from '@wrongstack/core/types';
|
|
2
|
+
import { type TaskItem } from '@wrongstack/core/utils';
|
|
3
|
+
import type { Tool } from '@wrongstack/core/types';
|
|
3
4
|
interface TaskInput {
|
|
4
5
|
/** Replace: set new task list. Add: append a task. Status: update task status. Promote: convert a task to todo items. */
|
|
5
6
|
action: 'replace' | 'add' | 'status' | 'show' | 'promote' | 'planify';
|
package/dist/task.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../src/task.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../src/task.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,wBAAwB,CAAC;AAQhC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAsCnD,UAAU,SAAS;IACjB,yHAAyH;IACzH,MAAM,EAAE,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IACtE,yCAAyC;IACzC,KAAK,CAAC,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG,SAAS,CAAC;IACpE,8DAA8D;IAC9D,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,oCAAoC;IACpC,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAC/B;AAED,UAAU,UAAU;IAClB,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAgbhD,CAAC"}
|
package/dist/task.js
CHANGED
|
@@ -2,26 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
computeTaskItemProgress,
|
|
4
4
|
formatTaskList
|
|
5
|
-
} from "@wrongstack/core";
|
|
6
|
-
import {
|
|
7
|
-
mutateTasks as mutateTasks2
|
|
8
|
-
} from "@wrongstack/core";
|
|
5
|
+
} from "@wrongstack/core/utils";
|
|
6
|
+
import { mutateTasks as mutateTasks2 } from "@wrongstack/core/storage";
|
|
9
7
|
import {
|
|
10
8
|
addPlanItem,
|
|
11
9
|
mutatePlan as mutatePlan2,
|
|
12
10
|
formatPlan
|
|
13
|
-
} from "@wrongstack/core";
|
|
11
|
+
} from "@wrongstack/core/storage";
|
|
14
12
|
import { randomUUID } from "node:crypto";
|
|
15
13
|
|
|
16
14
|
// src/session-kanban.ts
|
|
15
|
+
import { GlobalMailbox } from "@wrongstack/core/coordination";
|
|
17
16
|
import {
|
|
18
|
-
deserializeTaskGraph,
|
|
19
|
-
GlobalMailbox,
|
|
20
17
|
loadPlan,
|
|
21
18
|
loadTasks,
|
|
22
19
|
mutatePlan,
|
|
23
20
|
mutateTasks
|
|
24
|
-
} from "@wrongstack/core";
|
|
21
|
+
} from "@wrongstack/core/storage";
|
|
22
|
+
import { deserializeTaskGraph } from "@wrongstack/core/tasking";
|
|
25
23
|
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
26
24
|
import {
|
|
27
25
|
createBoard,
|
package/dist/task.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/task.ts", "../src/session-kanban.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n type TaskItem,\n type TaskStatus,\n type TaskFile,\n computeTaskItemProgress,\n formatTaskList,\n} from '@wrongstack/core';\nimport {\n mutateTasks,\n} from '@wrongstack/core';\nimport {\n addPlanItem,\n mutatePlan,\n formatPlan,\n} from '@wrongstack/core';\nimport { randomUUID } from 'node:crypto';\nimport type { Tool } from '@wrongstack/core';\nimport { projectSessionTasksToKanban } from './session-kanban.js';\n\n// ---------------------------------------------------------------------------\n// Task tool \u2014 structured work items with dependencies, types, and priorities.\n// Unlike `todo` (flat, session-scoped), tasks support:\n// - Dependencies (task can depend on other tasks)\n// - Type classification (feature, bugfix, refactor, docs, test, chore)\n// - Priority ranking (critical, high, medium, low)\n// - Assignment (which agent/subagent)\n// - Estimates (hours)\n//\n// Like `todo`, the list is fully replaced on every call. Session-persistent:\n// stored at `ctx.meta['task.path']` and isolated to this session \u2014 other sessions\n// have their own separate task lists.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Find a task by 1-based index, exact id, or case-insensitive title substring. */\nfunction findTaskIndex(tasks: TaskItem[], query: string): number {\n const asNum = Number.parseInt(query, 10);\n if (!Number.isNaN(asNum)) {\n const idx = asNum - 1;\n if (tasks[idx]) return idx;\n }\n const byId = tasks.findIndex((t) => t.id === query);\n if (byId >= 0) return byId;\n const lower = query.toLowerCase();\n return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));\n}\n\n// ---------------------------------------------------------------------------\n// Tool\n// ---------------------------------------------------------------------------\n\ninterface TaskInput {\n /** Replace: set new task list. Add: append a task. Status: update task status. Promote: convert a task to todo items. */\n action: 'replace' | 'add' | 'status' | 'show' | 'promote' | 'planify';\n /** Full task list for action=replace. */\n tasks?: TaskItem[] | undefined;\n /** Single task for action=add. id, createdAt, updatedAt are auto-generated. */\n task?: Omit<TaskItem, 'id' | 'createdAt' | 'updatedAt'> | undefined;\n /** Task id for action=status or target for action=promote. */\n id?: string | undefined;\n /** New status for action=status. */\n status?: TaskStatus | undefined;\n /** Target task (id, 1-based index, or title substring) for action=promote. */\n target?: string | undefined;\n /** Optional subtask titles for action=promote. */\n subtasks?: string[] | undefined;\n /**\n * Storage scope. Default (unset): uses the session-scoped path \u2014 isolated to this\n * session, survives resume within the same session.\n * `scope: 'project'`: uses a shared project-level path, visible to all sessions\n * for this project. Useful for a shared backlog that outlasts any single session.\n */\n scope?: 'session' | 'project';\n}\n\ninterface TaskOutput {\n ok: boolean;\n message: string;\n count: number;\n completed: number;\n inProgress: number;\n}\n\nexport const taskTool: Tool<TaskInput, TaskOutput> = {\n name: 'task',\n category: 'Session',\n description:\n 'Manage session-persistent structured work items with dependencies, types, and priorities. ' +\n 'Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), ' +\n 'dependencies between items, priority ranking, and agent assignment. ' +\n 'Tasks are written to disk and survive session resumes. By default they are isolated to this session; ' +\n 'use `scope: \"project\"` to store tasks in a shared project-level file visible to all sessions.',\n usageHint:\n 'USE FOR STRUCTURED WORK:\\n' +\n '- `action: \"replace\"` \u2014 set the complete task list (tasks ordered by priority)\\n' +\n '- `action: \"add\"` \u2014 append a single task\\n' +\n '- `action: \"status\"` \u2014 update a task\\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\\n' +\n '- `action: \"show\"` \u2014 view current tasks without changing them\\n' +\n '- `action: \"promote\"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\\n' +\n '- `action: \"planify\"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\\n\\n' +\n 'Task fields:\\n' +\n '- `dependsOn`: list of task IDs this one waits for\\n' +\n '- `type`: \"feature\" | \"bugfix\" | \"refactor\" | \"docs\" | \"test\" | \"chore\"\\n' +\n '- `priority`: \"critical\" | \"high\" | \"medium\" | \"low\"\\n' +\n '- `assignee`: agent/subagent name (e.g. \"bug-hunter\", \"refactor-planner\")\\n' +\n '- `estimateHours`: rough time estimate\\n' +\n '- `scope`: \"session\" (default, isolated) or \"project\" (shared across sessions)',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'task',\n timeoutMs: 2_000,\n inputSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['replace', 'add', 'status', 'show', 'promote', 'planify'],\n description: 'replace = set full list, add = append, status = update task status, show = view only, promote = convert task to todos, planify = convert task to plan item.',\n },\n tasks: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Unique id (e.g. \"t1\", \"auth-flow\").' },\n title: { type: 'string', description: 'Short title.' },\n description: { type: 'string', description: 'Optional details.' },\n type: { type: 'string', enum: ['feature', 'bugfix', 'refactor', 'docs', 'test', 'chore'] },\n priority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },\n status: { type: 'string', enum: ['pending', 'in_progress', 'blocked', 'failed', 'review', 'completed'] },\n dependsOn: {\n type: 'array',\n items: { type: 'string' },\n description: 'IDs of tasks this one depends on.',\n },\n assignee: { type: 'string', description: 'Agent/subagent assigned.' },\n estimateHours: { type: 'number', description: 'Estimated hours.' },\n tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },\n createdAt: { type: 'string' },\n updatedAt: { type: 'string' },\n },\n required: ['id', 'title', 'type', 'priority', 'status'],\n },\n description: 'Complete task list. Replaces previous list entirely.',\n },\n task: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n type: { type: 'string', enum: ['feature', 'bugfix', 'refactor', 'docs', 'test', 'chore'] },\n priority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },\n status: { type: 'string', enum: ['pending', 'in_progress', 'blocked', 'failed', 'review', 'completed'] },\n dependsOn: { type: 'array', items: { type: 'string' } },\n assignee: { type: 'string' },\n estimateHours: { type: 'number' },\n tags: { type: 'array', items: { type: 'string' } },\n },\n required: ['title', 'type', 'priority'],\n description: 'Single task to append (id/createdAt/updatedAt auto-generated).',\n },\n id: { type: 'string', description: 'Task id for action=status or target for action=promote.' },\n status: {\n type: 'string',\n enum: ['pending', 'in_progress', 'blocked', 'failed', 'review', 'completed'],\n description: 'New status for action=status.',\n },\n target: {\n type: 'string',\n description: 'Target task identifier (id, 1-based index, or title substring) for action=promote.',\n },\n subtasks: {\n type: 'array',\n items: { type: 'string' },\n description: 'Optional subtask titles for action=promote. Each becomes a pending todo.',\n },\n scope: {\n type: 'string',\n enum: ['session', 'project'],\n description: 'Storage scope: \"session\" (default, isolated to this session) or \"project\" (shared across all sessions for this project).',\n },\n },\n required: ['action'],\n },\n async execute(input, ctx) {\n const sessionTaskPath = (ctx.meta as Record<string, unknown>)['task.path'] as string | undefined;\n let taskPath: string | undefined;\n\n if (input.scope === 'project') {\n // Project-level: derive from the session path by replacing the filename with\n // 'backlog.tasks.json' so all sessions share the same file.\n if (typeof sessionTaskPath === 'string') {\n // Handle BOTH separators \u2014 a Windows-native path uses '\\\\'; a '/'-only\n // search would miss it and fall back to a bare relative path written\n // into the process CWD instead of the sessions dir.\n const lastSep = Math.max(sessionTaskPath.lastIndexOf('/'), sessionTaskPath.lastIndexOf('\\\\'));\n taskPath = lastSep >= 0\n ? sessionTaskPath.slice(0, lastSep + 1) + 'backlog.tasks.json'\n : 'backlog.tasks.json';\n }\n } else {\n taskPath = sessionTaskPath;\n }\n\n if (typeof taskPath !== 'string' || !taskPath) {\n return { ok: false, message: 'Task storage path not configured.', count: 0, completed: 0, inProgress: 0 };\n }\n const sessionId = ctx.session?.id ?? 'unknown';\n\n // Early-return result for validation errors that happen before or\n // during the critical section. The lock callback sets this instead of\n // mutating the file, and we return it after the lock releases.\n let early: TaskOutput | null = null;\n // Track promote output for the custom message\n const promoteMeta = { count: 0, title: '' };\n // Track planify data \u2014 written to plan file after the task lock releases\n const planifyMeta = { title: '', details: '' };\n let didPlanify = false;\n // collect todos to replace \u2014 called AFTER mutateTasks so rollback is possible\n type TodosReplacement = Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm?: string; promotedFromTask?: string }>;\n let todosToReplace: TodosReplacement | null = null;\n\n let file: TaskFile;\n try {\n file = await mutateTasks(taskPath, sessionId, async (f: TaskFile) => {\n switch (input.action) {\n case 'show':\n // read-only \u2014 no mutation, just return current state\n break;\n\n case 'replace': {\n if (!Array.isArray(input.tasks)) {\n early = { ok: false, message: 'action=replace requires `tasks` array.', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n // Validate id uniqueness: findTaskIndex / status resolve a task by\n // the FIRST id match, so a duplicate id silently becomes unaddressable.\n const newIds = new Set(input.tasks.map((t) => t.id));\n if (newIds.size !== input.tasks.length) {\n const seen = new Set<string>();\n const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => {\n if (seen.has(id)) return true;\n seen.add(id);\n return false;\n }))];\n early = {\n ok: false,\n message: `action=replace has duplicate task IDs: ${dupes.join(', ')}. Each task id must be unique.`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n return f;\n }\n // Validate dependsOn references: must point to IDs within the new batch\n for (const t of input.tasks) {\n if (t.dependsOn && t.dependsOn.length > 0) {\n const missing = t.dependsOn.filter((d) => !newIds.has(d));\n if (missing.length > 0) {\n early = {\n ok: false,\n message: `dependsOn validation failed: task \"${t.id}\" references unknown IDs: ${missing.join(', ')}`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n return f;\n }\n }\n }\n const now = new Date().toISOString();\n f.tasks = input.tasks.map((t) => ({\n ...t,\n createdAt: t.createdAt || now,\n updatedAt: now,\n }));\n break;\n }\n\n case 'add': {\n const t = input.task;\n if (!t?.title) {\n early = { ok: false, message: 'action=add requires `task` with at least `title`.', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n // Validate dependsOn: all referenced IDs must exist in the current task list\n if (t.dependsOn && t.dependsOn.length > 0) {\n const existingIds = new Set(f.tasks.map((e: TaskItem) => e.id));\n const missing = t.dependsOn.filter((d) => !existingIds.has(d));\n if (missing.length > 0) {\n early = {\n ok: false,\n message: `dependsOn validation failed: unknown task IDs: ${missing.join(', ')}`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n return f;\n }\n }\n const now = new Date().toISOString();\n const newTask: TaskItem = {\n id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,\n title: t.title,\n description: t.description,\n type: t.type || 'feature',\n priority: t.priority || 'medium',\n status: t.status || 'pending',\n dependsOn: t.dependsOn,\n assignee: t.assignee,\n estimateHours: t.estimateHours,\n tags: t.tags,\n createdAt: now,\n updatedAt: now,\n };\n f.tasks.push(newTask);\n break;\n }\n\n case 'status': {\n if (!input.id || !input.status) {\n early = { ok: false, message: 'action=status requires `id` and `status`.', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const task = f.tasks.find((t: TaskItem) => t.id === input.id);\n if (!task) {\n early = { ok: false, message: `Task \"${input.id}\" not found.`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n task.status = input.status;\n task.updatedAt = new Date().toISOString();\n break;\n }\n\n case 'promote': {\n const target = input.target?.trim();\n if (!target) {\n early = { ok: false, message: 'action=promote requires `target` (task id, index, or title substring).', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const idx = findTaskIndex(f.tasks, target);\n if (idx === -1) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const match = f.tasks[idx];\n /* v8 ignore next 4 -- findTaskIndex returned a valid in-range idx, so match is always defined; defensive. */\n if (!match) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n\n // Mark task in_progress\n if (match.status !== 'completed' && match.status !== 'failed') {\n match.status = 'in_progress';\n match.updatedAt = new Date().toISOString();\n }\n\n // Build todo items\n const todos: Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm?: string; promotedFromTask?: string }> = [];\n const ts = Date.now();\n todos.push({\n id: `todo_${ts}_task`,\n content: match.title,\n status: 'in_progress',\n activeForm: match.title,\n promotedFromTask: match.id,\n });\n\n if (match.description) {\n todos.push({\n id: `todo_${ts}_${randomUUID().slice(0, 6)}`,\n content: match.description.slice(0, 200),\n status: 'pending',\n promotedFromTask: match.id,\n });\n }\n\n if (input.subtasks && input.subtasks.length > 0) {\n for (const st of input.subtasks) {\n todos.push({\n id: `todo_${ts}_${randomUUID().slice(0, 6)}`,\n content: st,\n status: 'pending',\n promotedFromTask: match.id,\n });\n }\n }\n\n todosToReplace = todos;\n promoteMeta.count = todos.length;\n promoteMeta.title = match.title;\n break;\n }\n\n case 'planify': {\n const target = input.target?.trim();\n if (!target) {\n early = { ok: false, message: 'action=planify requires `target` (task id, index, or title substring).', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const idx = findTaskIndex(f.tasks, target);\n if (idx === -1) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const match = f.tasks[idx];\n /* v8 ignore next 4 -- findTaskIndex returned a valid in-range idx, so match is always defined; defensive. */\n if (!match) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n // Extract data \u2014 plan write happens after the task lock releases\n planifyMeta.title = match.title;\n planifyMeta.details = match.description ?? '';\n didPlanify = true;\n // Do NOT mutate the task \u2014 just copy to plan\n break;\n }\n\n default:\n early = { ok: false, message: `Unknown action \"${(input as { action: string }).action}\". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n\n return f;\n });\n } catch (err) {\n // Persist failed (mutateTasks throws on a failed save) \u2014 report ok:false\n // instead of falsely claiming the tasks were saved.\n return {\n ok: false,\n message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n }\n\n // Apply todo replacements after the task file mutation succeeds so that\n // on error the state is rolled back cleanly.\n if (todosToReplace) ctx.state.replaceTodos(todosToReplace);\n\n // A successful task mutation includes its Kanban projection. Awaiting this\n // keeps the session board authoritative at tool-return time.\n await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);\n\n // If the callback set an early-return result, use it\n if (early) return early;\n\n // If planify copied task data, write it to the plan file now\n if (didPlanify) {\n const { title, details } = planifyMeta;\n const planPathRaw = (ctx.meta as Record<string, unknown>)['plan.path'];\n const prog = computeTaskItemProgress(file.tasks);\n if (typeof planPathRaw === 'string' && planPathRaw) {\n let planPath: string = planPathRaw;\n // Honor project scope for the PLAN file too (mirror of plan.ts taskify);\n // handle both separators.\n if (input.scope === 'project') {\n const lastSep = Math.max(planPath.lastIndexOf('/'), planPath.lastIndexOf('\\\\'));\n planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + 'backlog.plan.json' : 'backlog.plan.json';\n }\n // Mutate the cross-file under ITS OWN lock so a concurrent plan tool\n // call in the same batch can't clobber the write.\n let formatted = '';\n try {\n await mutatePlan(planPath, sessionId, (pf) => {\n const { plan: updated } = addPlanItem(pf, title, details || undefined);\n formatted = formatPlan(updated);\n return updated;\n });\n } catch (err) {\n return {\n ok: false,\n message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,\n count: file.tasks.length,\n completed: prog.completed,\n inProgress: prog.inProgress,\n };\n }\n return {\n ok: true,\n message: `planify ok \u2014 added \"${title}\" to plan.\\n${formatted}`,\n count: file.tasks.length,\n completed: prog.completed,\n inProgress: prog.inProgress,\n };\n }\n // Plan path missing \u2014 still report the REAL task counts (the task file was\n // loaded and may be non-empty), not zeros.\n return {\n ok: false,\n message: 'Plan storage path not configured \u2014 cannot planify.',\n count: file.tasks.length,\n completed: prog.completed,\n inProgress: prog.inProgress,\n };\n }\n\n const p = computeTaskItemProgress(file.tasks);\n const summary = promoteMeta.count > 0\n ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from \"${promoteMeta.title}\".\\n${formatTaskList(file.tasks)}`\n : file.tasks.length > 0\n ? formatTaskList(file.tasks)\n : 'No tasks.';\n return {\n ok: true,\n message: summary,\n count: file.tasks.length,\n completed: p.completed,\n inProgress: p.inProgress,\n };\n },\n};\n", "import { type FSWatcher, watch } from 'node:fs';\nimport { basename, dirname } from 'node:path';\nimport {\n type Context,\n deserializeTaskGraph,\n GlobalMailbox,\n loadPlan,\n loadTasks,\n mutatePlan,\n mutateTasks,\n type PlanFile,\n type PlanItem,\n type SerializedTaskGraph,\n type TaskFile,\n type TaskItem,\n type TaskStatus,\n type TodoItem,\n} from '@wrongstack/core';\nimport { resolveWstackPaths } from '@wrongstack/core/utils';\nimport {\n createBoard,\n getBoard,\n getKanbanDir,\n type KanbanBoard,\n type KanbanColumn,\n type KanbanTask,\n listBoards,\n removeBoard,\n syncBoardFromTaskGraph,\n touchKanbanPresence,\n updateBoard,\n} from '@wrongstack/kanban';\n\nconst SESSION_BOARD_TAG = 'session-work';\nconst MIRROR_DISABLED_ENV = 'WRONGSTACK_KANBAN_TASK_MIRROR';\n\n/** The canonical workflow shared by WebUI and TUI session boards. */\nexport const SESSION_KANBAN_COLUMNS: KanbanColumn[] = [\n { id: 'todo', title: 'Todo', order: 0, wipLimit: 0, color: '#2563eb' },\n { id: 'in-progress', title: 'Running', order: 1, wipLimit: 1, color: '#d97706' },\n { id: 'review', title: 'Preview', order: 2, wipLimit: 0, color: '#7c3aed' },\n { id: 'done', title: 'Done', order: 3, wipLimit: 0, color: '#16a34a' },\n];\n\nconst boardQueue = new Map<string, Promise<void>>();\nconst boardEnsures = new Map<string, Promise<KanbanBoard>>();\ntype PendingMirror = {\n projectRoot: string;\n sessionId: string;\n graph: SerializedTaskGraph;\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan';\n};\nconst pendingMirrors = new Map<string, PendingMirror>();\nconst activeMirrors = new Set<string>();\nconst bindings = new WeakMap<Context, () => void>();\nconst suppressedTodoMirrors = new WeakSet<Context>();\nconst activeSessionBoards = new Map<string, number>();\n\nfunction boardKey(projectRoot: string, sessionId: string): string {\n return `${projectRoot}\\0${sessionId}`;\n}\n\nfunction mirrorKey(\n projectRoot: string,\n sessionId: string,\n sourceSystem: PendingMirror['sourceSystem'],\n): string {\n return `${boardKey(projectRoot, sessionId)}\\0${sourceSystem}`;\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction sessionBoardTitle(sessionId: string): string {\n const leaf = sessionId.split(/[\\\\/]/).filter(Boolean).pop() ?? sessionId;\n return `Session ${leaf.slice(0, 12)}`;\n}\n\nfunction sessionBoardTags(sessionId: string): string[] {\n return ['session', SESSION_BOARD_TAG, sessionTag(sessionId)];\n}\n\nfunction sessionIdFromTags(tags: readonly string[] | undefined): string | null {\n const tag = tags?.find((candidate) => candidate.startsWith('session:'));\n return tag?.slice('session:'.length) || null;\n}\n\nfunction isOwnedSessionBoard(tags: readonly string[] | undefined): boolean {\n return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));\n}\n\nfunction retainActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);\n}\n\nfunction releaseActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n const remaining = (activeSessionBoards.get(key) ?? 0) - 1;\n if (remaining > 0) activeSessionBoards.set(key, remaining);\n else activeSessionBoards.delete(key);\n}\n\nfunction isSessionBoardActive(projectRoot: string, sessionId: string): boolean {\n return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;\n}\n\nfunction sameColumns(columns: readonly KanbanColumn[]): boolean {\n return (\n columns.length === SESSION_KANBAN_COLUMNS.length &&\n columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id)\n );\n}\n\n/** Create (or migrate) the single Kanban board owned by a session. */\nexport async function ensureSessionKanbanBoard(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n const key = boardKey(projectRoot, sessionId);\n const inFlight = boardEnsures.get(key);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n const summary = (await listBoards(projectRoot)).find((board) =>\n board.tags?.includes(sessionTag(sessionId)),\n );\n let board = summary ? await getBoard(projectRoot, summary.id) : null;\n if (!board) {\n return createBoard(projectRoot, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: sessionBoardTags(sessionId),\n columns: SESSION_KANBAN_COLUMNS,\n generatedBy: `session-kanban:${sessionId}`,\n });\n }\n\n // Boards produced by the older task/plan mirrors used the generic five\n // columns. Migrate only session-owned boards; updateBoard reconciles cards\n // from Backlog into Todo while preserving Running/Preview/Done cards.\n if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {\n board =\n (await updateBoard(projectRoot, board.id, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n columns: SESSION_KANBAN_COLUMNS,\n })) ?? board;\n }\n return board;\n })();\n\n boardEnsures.set(key, promise);\n try {\n return await promise;\n } finally {\n boardEnsures.delete(key);\n }\n}\n\nfunction enqueueBoardWork<T>(\n projectRoot: string,\n sessionId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const key = boardKey(projectRoot, sessionId);\n const previous = boardQueue.get(key) ?? Promise.resolve();\n const result = previous.catch(() => undefined).then(work);\n const tail = result.then(\n () => undefined,\n () => undefined,\n );\n boardQueue.set(key, tail);\n void tail.then(() => {\n if (boardQueue.get(key) === tail) boardQueue.delete(key);\n });\n return result;\n}\n\nasync function removeEmptySessionBoard(\n projectRoot: string,\n boardId: string,\n sessionId: string,\n): Promise<string | null> {\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n if (isSessionBoardActive(projectRoot, sessionId)) return null;\n const board = await getBoard(projectRoot, boardId);\n if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;\n if (sessionIdFromTags(board.tags) !== sessionId) return null;\n return (await removeBoard(projectRoot, board.id)) ? board.id : null;\n });\n}\n\n/** Remove a particular inactive session's system-owned board when it has no cards. */\nexport async function cleanupSessionKanbanBoardIfEmpty(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<string[]> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n if (isSessionBoardActive(projectRoot, sessionId)) return [];\n const candidates = (await listBoards(projectRoot)).filter(\n (board) =>\n board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n sessionIdFromTags(board.tags) === sessionId,\n );\n const removed = await Promise.all(\n candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId)),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\n/** Prune stale empty session boards while preserving manual and live boards. */\nexport async function cleanupEmptySessionKanbanBoards(\n projectRoot: string | undefined,\n activeSessionId = '',\n): Promise<string[]> {\n if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n const candidates = (await listBoards(projectRoot)).flatMap((board) => {\n const ownerSessionId = sessionIdFromTags(board.tags);\n return board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n ownerSessionId &&\n ownerSessionId !== activeSessionId &&\n !isSessionBoardActive(projectRoot, ownerSessionId)\n ? [{ boardId: board.id, sessionId: ownerSessionId }]\n : [];\n });\n const removed = await Promise.all(\n candidates.map(({ boardId, sessionId }) =>\n removeEmptySessionBoard(projectRoot, boardId, sessionId),\n ),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\nasync function projectGraph(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan',\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n const board = await ensureSessionKanbanBoard(projectRoot, sessionId);\n if (!board) return null;\n const result = await syncBoardFromTaskGraph(\n projectRoot,\n board.id,\n deserializeTaskGraph(graph),\n {\n sourceSystem,\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n archiveMissingTasks: true,\n includeCompletedTasks: true,\n },\n );\n return result?.board ?? null;\n });\n}\n\n/**\n * Queue an observational mirror without retaining every intermediate state.\n * Todo/task/plan streams are independent, but within each stream only the\n * newest pending graph matters. This bounds a stalled file lock to one active\n * and one pending projection instead of an arbitrarily long Promise chain.\n */\nfunction queueLatestMirror(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: PendingMirror['sourceSystem'],\n): void {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return;\n const key = mirrorKey(projectRoot, sessionId, sourceSystem);\n pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });\n if (activeMirrors.has(key)) return;\n activeMirrors.add(key);\n void (async () => {\n try {\n for (;;) {\n const pending = pendingMirrors.get(key);\n if (!pending) break;\n pendingMirrors.delete(key);\n try {\n await projectGraph(\n pending.projectRoot,\n pending.sessionId,\n pending.graph,\n pending.sourceSystem,\n );\n } catch {\n // Mirrors are observational. A newer pending snapshot, if present,\n // still gets a chance after a transient lock or filesystem failure.\n }\n }\n } finally {\n activeMirrors.delete(key);\n // A mirror can arrive between the last Map read and deleting the active\n // marker. Re-arm once so that snapshot cannot be stranded.\n const pending = pendingMirrors.get(key);\n if (pending) {\n pendingMirrors.delete(key);\n queueLatestMirror(\n pending.projectRoot,\n pending.sessionId,\n pending.graph,\n pending.sourceSystem,\n );\n }\n }\n })();\n}\n\nexport function todoListToSerializedGraph(\n todos: readonly TodoItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = todos.map((todo, index) => ({\n id: todo.id,\n title: todo.content,\n description: todo.activeForm ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: todo.status,\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `todo:${sessionId}`,\n specId: `todo:${sessionId}`,\n title: 'Session todos',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function taskFileToSerializedGraph(\n tasks: readonly TaskItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const ids = new Set(tasks.map((task) => task.id));\n const nodes = tasks.map((task, index) => ({\n id: task.id,\n title: task.title,\n description: task.description ?? '',\n type: task.type,\n priority: task.priority,\n status: task.status,\n ...(task.assignee ? { assignee: task.assignee } : {}),\n ...(task.estimateHours !== undefined ? { estimateHours: task.estimateHours } : {}),\n createdAt: index,\n updatedAt: index,\n }));\n const edges = tasks.flatMap((task) =>\n (task.dependsOn ?? [])\n .filter((dependency) => ids.has(dependency))\n .map((dependency) => ({\n id: `${dependency}->${task.id}`,\n from: dependency,\n to: task.id,\n type: 'depends_on' as const,\n })),\n );\n const hasIncoming = new Set(edges.map((edge) => edge.to));\n const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);\n return {\n // Keep the historical graph id so existing mirrored task cards are reused.\n id: `session:${sessionId}`,\n specId: `session:${sessionId}`,\n title: 'Session tasks',\n nodes,\n edges,\n rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nconst PLAN_STATUS_TO_TASK: Record<PlanItem['status'], TaskStatus> = {\n open: 'pending',\n in_progress: 'in_progress',\n done: 'completed',\n};\n\nexport function planFileToSerializedGraph(\n items: readonly PlanItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = items.map((item, index) => ({\n id: item.id,\n title: item.title,\n description: item.details ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: PLAN_STATUS_TO_TASK[item.status],\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `plan:${sessionId}`,\n specId: `plan:${sessionId}`,\n title: 'Session plan',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function projectSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function projectSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function projectSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\nfunction fireAndForget(work: Promise<unknown>): void {\n void work.catch(() => {\n // The session files/state remain recoverable if the observational board\n // cannot be written; the next mutation or file watcher retries the mirror.\n });\n}\n\nfunction broadcastTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const sessionId = context.session?.id ?? '';\n if (!context.agentId || !sessionId) return;\n const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;\n const mailbox = new GlobalMailbox(projectDir);\n void mailbox\n .send({\n from: context.agentId,\n to: '*',\n type: 'status',\n subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? '' : 's'})`,\n body: JSON.stringify({\n kind: 'kanban.todos.updated',\n sessionId,\n revision: context.state.revision,\n todos,\n }),\n priority: 'normal',\n senderSessionId: sessionId,\n ttlMs: 6 * 60 * 60 * 1000,\n })\n .catch(() => {\n // Mailbox awareness is best-effort; canonical state is already updated.\n });\n}\n\nfunction notifyTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const summary = todos.length\n ? todos\n .map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`)\n .join('\\n')\n : '- No active todos remain.';\n const text = `[KANBAN TODO UPDATE]\\nAnother Kanban agent reassessed the shared board. The canonical todo list is now:\\n${summary}\\nReassess your current plan before continuing; do not rely on the initial todo snapshot.`;\n const state = context.state as Partial<Context['state']>;\n if (typeof state.appendBlockToLastUserMessage === 'function') {\n if (state.appendBlockToLastUserMessage({ type: 'text', text })) return;\n }\n if (typeof state.appendMessage === 'function') {\n state.appendMessage({ role: 'user', content: [{ type: 'text', text }] });\n }\n}\n\nexport function mirrorSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function mirrorSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function mirrorSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\n/**\n * Bind all live session work paths to Kanban. Todo changes are observed from\n * ConversationState; plan/task sidecars are watched so slash commands, WebUI,\n * plugins, and tools all pass through the same board.\n */\nexport function attachSessionKanbanMirror(context: Context): () => void {\n const existing = bindings.get(context);\n if (existing) return existing;\n\n const attachedProjectRoot = context.projectRoot ?? '';\n let registeredSessionId = '';\n const syncActiveSessionRegistration = () => {\n if (!attachedProjectRoot) return;\n const currentSessionId = context.session?.id ?? '';\n if (currentSessionId === registeredSessionId) return;\n if (registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n }\n registeredSessionId = currentSessionId;\n if (registeredSessionId) {\n retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n }\n };\n syncActiveSessionRegistration();\n\n let watcher: FSWatcher | null = null;\n let watchedDir = '';\n let timer: NodeJS.Timeout | null = null;\n let boardWatcher: FSWatcher | null = null;\n let watchedBoardId = '';\n let boardTimer: NodeJS.Timeout | null = null;\n let presenceTimer: NodeJS.Timeout | null = null;\n\n const sessionId = () => context.session?.id ?? '';\n const refreshFiles = async () => {\n const id = sessionId();\n if (!id) return;\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n };\n\n const refreshBoard = async () => {\n if (!watchedBoardId) return;\n const board = await getBoard(context.projectRoot, watchedBoardId);\n if (board) applySessionKanbanBoardToTodos(context, board);\n };\n\n const configureBoardWatcher = async () => {\n const id = sessionId();\n const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;\n if (!board || board.id === watchedBoardId) return;\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = board.id;\n try {\n const boardFileName = `${board.id}.json`;\n boardWatcher = watch(\n getKanbanDir(context.projectRoot),\n { persistent: false },\n (_event, filename) => {\n if (filename?.toString() !== boardFileName) return;\n if (boardTimer) clearTimeout(boardTimer);\n boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);\n },\n );\n const touchPresence = () =>\n touchKanbanPresence(context.projectRoot, board.id, {\n sessionId: id,\n agentId: context.agentId,\n agentName: context.agentName,\n });\n fireAndForget(touchPresence());\n if (presenceTimer) clearInterval(presenceTimer);\n presenceTimer = setInterval(() => fireAndForget(touchPresence()), 60_000);\n presenceTimer.unref?.();\n boardWatcher.on('error', () => {\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = '';\n });\n } catch {\n boardWatcher = null;\n watchedBoardId = '';\n }\n };\n\n const configureWatcher = () => {\n const planPath = context.meta['plan.path'];\n const taskPath = context.meta['task.path'];\n const candidate =\n typeof planPath === 'string' && planPath\n ? dirname(planPath)\n : typeof taskPath === 'string' && taskPath\n ? dirname(taskPath)\n : '';\n if (!candidate || candidate === watchedDir) return;\n watcher?.close();\n watcher = null;\n watchedDir = candidate;\n try {\n watcher = watch(candidate, { persistent: false }, (_event, filename) => {\n const name = filename?.toString();\n const currentPlanPath = context.meta['plan.path'];\n const currentTaskPath = context.meta['task.path'];\n const planName = typeof currentPlanPath === 'string' ? basename(currentPlanPath) : '';\n const taskName = typeof currentTaskPath === 'string' ? basename(currentTaskPath) : '';\n if (name && name !== planName && name !== taskName) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fireAndForget(refreshFiles()), 60);\n });\n watcher.on('error', () => watcher?.close());\n } catch {\n // The session directory can briefly disappear during project/session\n // switches; the next meta update re-attempts the binding.\n watcher = null;\n watchedDir = '';\n }\n };\n\n const unsubscribe = context.state.onChange((change) => {\n if (change.kind === 'todos_replaced' && !suppressedTodoMirrors.has(context)) {\n // ConversationState auto-clears an all-done tactical list. Project the\n // pre-clear completion snapshot so every card reaches Done atomically.\n mirrorSessionTodosToKanban(\n context.projectRoot,\n change.completedSnapshot ?? change.todos,\n sessionId(),\n );\n return;\n }\n if (change.kind === 'meta_set' && (change.key === 'plan.path' || change.key === 'task.path')) {\n syncActiveSessionRegistration();\n configureWatcher();\n fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));\n fireAndForget(configureBoardWatcher());\n fireAndForget(refreshFiles());\n }\n });\n\n configureWatcher();\n fireAndForget(configureBoardWatcher());\n\n const detach = () => {\n unsubscribe();\n if (timer) clearTimeout(timer);\n if (boardTimer) clearTimeout(boardTimer);\n if (presenceTimer) clearInterval(presenceTimer);\n watcher?.close();\n boardWatcher?.close();\n bindings.delete(context);\n if (attachedProjectRoot && registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n registeredSessionId = '';\n }\n };\n bindings.set(context, detach);\n return detach;\n}\n\n/** Fully hydrate a session board before a host announces the session as ready. */\nexport async function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null> {\n const id = context.session?.id ?? '';\n if (!id) return null;\n await cleanupEmptySessionKanbanBoards(context.projectRoot, id);\n let board = await ensureSessionKanbanBoard(context.projectRoot, id);\n if (context.todos.length) {\n board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);\n }\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n return board;\n}\n\nexport interface SessionKanbanSourceUpdate {\n source: 'todo' | 'task' | 'plan' | null;\n todos?: TodoItem[] | undefined;\n tasks?: TaskFile | undefined;\n plan?: PlanFile | undefined;\n}\n\nfunction sourceStatus(task: KanbanTask): TaskStatus {\n if (task.status === 'completed') return 'completed';\n if (task.status === 'in_progress') return 'in_progress';\n if (task.status === 'review') return 'review';\n if (task.status === 'blocked') return 'blocked';\n if (task.status === 'failed') return 'failed';\n return 'pending';\n}\n\nfunction todoStatus(task: KanbanTask): TodoItem['status'] {\n const status = sourceStatus(task);\n if (status === 'completed') return 'completed';\n if (status === 'in_progress' || status === 'review') return 'in_progress';\n return 'pending';\n}\n\nfunction sessionTodoFromTask(task: KanbanTask): TodoItem {\n return {\n id: task.origin?.taskId ?? task.id,\n content: task.title,\n status: todoStatus(task),\n ...(task.description ? { activeForm: task.description } : {}),\n };\n}\n\nfunction sameTodos(left: readonly TodoItem[], right: readonly TodoItem[]): boolean {\n return (\n left.length === right.length &&\n left.every((todo, index) => {\n const candidate = right[index];\n return (\n candidate?.id === todo.id &&\n candidate.content === todo.content &&\n candidate.status === todo.status &&\n candidate.activeForm === todo.activeForm &&\n candidate.promotedFromPlan === todo.promotedFromPlan &&\n candidate.promotedFromTask === todo.promotedFromTask\n );\n })\n );\n}\n\n/**\n * Replace the tactical todo list from the current cards on a session-owned board.\n *\n * Existing mirrored todo cards retain their stable source ids. New cards created\n * by a Kanban worker become todos under their card ids, so reassessment can add,\n * split, merge, reprioritize, or remove work without being overwritten by the\n * todo list that happened to exist when the run started.\n */\nexport function applySessionKanbanBoardToTodos(context: Context, board: KanbanBoard): TodoItem[] {\n const sessionId = context.session?.id ?? '';\n if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {\n return [...context.todos];\n }\n\n const projectedTodos = board.tasks\n .filter(\n (task) =>\n task.status !== 'archived' &&\n (!task.origin ||\n task.origin.system === 'session-todo' ||\n (task.origin.graphId ?? '').startsWith('todo:')),\n )\n .sort((left, right) => {\n const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;\n const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;\n return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);\n })\n .map(sessionTodoFromTask);\n\n const allCompleted =\n projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === 'completed');\n const effectiveTodos = allCompleted ? [] : projectedTodos;\n if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(projectedTodos);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n notifyTodoUpdate(context, context.todos);\n broadcastTodoUpdate(context, context.todos);\n return [...context.todos];\n}\n\n/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */\nexport async function applySessionKanbanTaskToSource(\n context: Context,\n task: KanbanTask,\n options: { remove?: boolean | undefined } = {},\n): Promise<SessionKanbanSourceUpdate> {\n const originId = task.origin?.taskId;\n const graphId = task.origin?.graphId ?? '';\n if (!originId) return { source: null };\n\n if (task.origin?.system === 'session-todo' || graphId.startsWith('todo:')) {\n const next = options.remove\n ? context.todos.filter((todo) => todo.id !== originId)\n : context.todos.map((todo) =>\n todo.id === originId\n ? {\n ...todo,\n content: task.title,\n status:\n sourceStatus(task) === 'completed'\n ? ('completed' as const)\n : sourceStatus(task) === 'in_progress' || sourceStatus(task) === 'review'\n ? ('in_progress' as const)\n : ('pending' as const),\n }\n : todo,\n );\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(next);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n return { source: 'todo', todos: [...context.todos] };\n }\n\n const id = context.session?.id ?? '';\n if (task.origin?.system === 'session-plan' || graphId.startsWith('plan:')) {\n const planPath = context.meta['plan.path'];\n if (typeof planPath !== 'string' || !planPath) return { source: 'plan' };\n const plan = await mutatePlan(planPath, id, (file) => ({\n ...file,\n updatedAt: new Date().toISOString(),\n items: options.remove\n ? file.items.filter((item) => item.id !== originId)\n : file.items.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n details: task.description,\n status:\n task.status === 'completed'\n ? ('done' as const)\n : task.status === 'in_progress' || task.status === 'review'\n ? ('in_progress' as const)\n : ('open' as const),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'plan', plan };\n }\n\n if (\n task.origin?.system === 'session-task' ||\n task.origin?.system === 'session' ||\n graphId.startsWith('session:')\n ) {\n const taskPath = context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) return { source: 'task' };\n const tasks = await mutateTasks(taskPath, id, (file) => ({\n ...file,\n tasks: options.remove\n ? file.tasks.filter((item) => item.id !== originId)\n : file.tasks.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n description: task.description,\n status: sourceStatus(task),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'task', tasks };\n }\n\n return { source: null };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { TaskStatus } from '@wrongstack/core/types';\nimport type { TaskFile } from '@wrongstack/core/storage';\nimport {\n computeTaskItemProgress,\n formatTaskList,\n type TaskItem,\n} from '@wrongstack/core/utils';\nimport { mutateTasks } from '@wrongstack/core/storage';\nimport {\n addPlanItem,\n mutatePlan,\n formatPlan,\n} from '@wrongstack/core/storage';\nimport { randomUUID } from 'node:crypto';\nimport type { Tool } from '@wrongstack/core/types';\nimport { projectSessionTasksToKanban } from './session-kanban.js';\n\n// ---------------------------------------------------------------------------\n// Task tool \u2014 structured work items with dependencies, types, and priorities.\n// Unlike `todo` (flat, session-scoped), tasks support:\n// - Dependencies (task can depend on other tasks)\n// - Type classification (feature, bugfix, refactor, docs, test, chore)\n// - Priority ranking (critical, high, medium, low)\n// - Assignment (which agent/subagent)\n// - Estimates (hours)\n//\n// Like `todo`, the list is fully replaced on every call. Session-persistent:\n// stored at `ctx.meta['task.path']` and isolated to this session \u2014 other sessions\n// have their own separate task lists.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Find a task by 1-based index, exact id, or case-insensitive title substring. */\nfunction findTaskIndex(tasks: TaskItem[], query: string): number {\n const asNum = Number.parseInt(query, 10);\n if (!Number.isNaN(asNum)) {\n const idx = asNum - 1;\n if (tasks[idx]) return idx;\n }\n const byId = tasks.findIndex((t) => t.id === query);\n if (byId >= 0) return byId;\n const lower = query.toLowerCase();\n return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));\n}\n\n// ---------------------------------------------------------------------------\n// Tool\n// ---------------------------------------------------------------------------\n\ninterface TaskInput {\n /** Replace: set new task list. Add: append a task. Status: update task status. Promote: convert a task to todo items. */\n action: 'replace' | 'add' | 'status' | 'show' | 'promote' | 'planify';\n /** Full task list for action=replace. */\n tasks?: TaskItem[] | undefined;\n /** Single task for action=add. id, createdAt, updatedAt are auto-generated. */\n task?: Omit<TaskItem, 'id' | 'createdAt' | 'updatedAt'> | undefined;\n /** Task id for action=status or target for action=promote. */\n id?: string | undefined;\n /** New status for action=status. */\n status?: TaskStatus | undefined;\n /** Target task (id, 1-based index, or title substring) for action=promote. */\n target?: string | undefined;\n /** Optional subtask titles for action=promote. */\n subtasks?: string[] | undefined;\n /**\n * Storage scope. Default (unset): uses the session-scoped path \u2014 isolated to this\n * session, survives resume within the same session.\n * `scope: 'project'`: uses a shared project-level path, visible to all sessions\n * for this project. Useful for a shared backlog that outlasts any single session.\n */\n scope?: 'session' | 'project';\n}\n\ninterface TaskOutput {\n ok: boolean;\n message: string;\n count: number;\n completed: number;\n inProgress: number;\n}\n\nexport const taskTool: Tool<TaskInput, TaskOutput> = {\n name: 'task',\n category: 'Session',\n description:\n 'Manage session-persistent structured work items with dependencies, types, and priorities. ' +\n 'Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), ' +\n 'dependencies between items, priority ranking, and agent assignment. ' +\n 'Tasks are written to disk and survive session resumes. By default they are isolated to this session; ' +\n 'use `scope: \"project\"` to store tasks in a shared project-level file visible to all sessions.',\n usageHint:\n 'USE FOR STRUCTURED WORK:\\n' +\n '- `action: \"replace\"` \u2014 set the complete task list (tasks ordered by priority)\\n' +\n '- `action: \"add\"` \u2014 append a single task\\n' +\n '- `action: \"status\"` \u2014 update a task\\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\\n' +\n '- `action: \"show\"` \u2014 view current tasks without changing them\\n' +\n '- `action: \"promote\"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\\n' +\n '- `action: \"planify\"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\\n\\n' +\n 'Task fields:\\n' +\n '- `dependsOn`: list of task IDs this one waits for\\n' +\n '- `type`: \"feature\" | \"bugfix\" | \"refactor\" | \"docs\" | \"test\" | \"chore\"\\n' +\n '- `priority`: \"critical\" | \"high\" | \"medium\" | \"low\"\\n' +\n '- `assignee`: agent/subagent name (e.g. \"bug-hunter\", \"refactor-planner\")\\n' +\n '- `estimateHours`: rough time estimate\\n' +\n '- `scope`: \"session\" (default, isolated) or \"project\" (shared across sessions)',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n icon: 'task',\n timeoutMs: 2_000,\n inputSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['replace', 'add', 'status', 'show', 'promote', 'planify'],\n description: 'replace = set full list, add = append, status = update task status, show = view only, promote = convert task to todos, planify = convert task to plan item.',\n },\n tasks: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Unique id (e.g. \"t1\", \"auth-flow\").' },\n title: { type: 'string', description: 'Short title.' },\n description: { type: 'string', description: 'Optional details.' },\n type: { type: 'string', enum: ['feature', 'bugfix', 'refactor', 'docs', 'test', 'chore'] },\n priority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },\n status: { type: 'string', enum: ['pending', 'in_progress', 'blocked', 'failed', 'review', 'completed'] },\n dependsOn: {\n type: 'array',\n items: { type: 'string' },\n description: 'IDs of tasks this one depends on.',\n },\n assignee: { type: 'string', description: 'Agent/subagent assigned.' },\n estimateHours: { type: 'number', description: 'Estimated hours.' },\n tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },\n createdAt: { type: 'string' },\n updatedAt: { type: 'string' },\n },\n required: ['id', 'title', 'type', 'priority', 'status'],\n },\n description: 'Complete task list. Replaces previous list entirely.',\n },\n task: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n type: { type: 'string', enum: ['feature', 'bugfix', 'refactor', 'docs', 'test', 'chore'] },\n priority: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },\n status: { type: 'string', enum: ['pending', 'in_progress', 'blocked', 'failed', 'review', 'completed'] },\n dependsOn: { type: 'array', items: { type: 'string' } },\n assignee: { type: 'string' },\n estimateHours: { type: 'number' },\n tags: { type: 'array', items: { type: 'string' } },\n },\n required: ['title', 'type', 'priority'],\n description: 'Single task to append (id/createdAt/updatedAt auto-generated).',\n },\n id: { type: 'string', description: 'Task id for action=status or target for action=promote.' },\n status: {\n type: 'string',\n enum: ['pending', 'in_progress', 'blocked', 'failed', 'review', 'completed'],\n description: 'New status for action=status.',\n },\n target: {\n type: 'string',\n description: 'Target task identifier (id, 1-based index, or title substring) for action=promote.',\n },\n subtasks: {\n type: 'array',\n items: { type: 'string' },\n description: 'Optional subtask titles for action=promote. Each becomes a pending todo.',\n },\n scope: {\n type: 'string',\n enum: ['session', 'project'],\n description: 'Storage scope: \"session\" (default, isolated to this session) or \"project\" (shared across all sessions for this project).',\n },\n },\n required: ['action'],\n },\n async execute(input, ctx) {\n const sessionTaskPath = (ctx.meta as Record<string, unknown>)['task.path'] as string | undefined;\n let taskPath: string | undefined;\n\n if (input.scope === 'project') {\n // Project-level: derive from the session path by replacing the filename with\n // 'backlog.tasks.json' so all sessions share the same file.\n if (typeof sessionTaskPath === 'string') {\n // Handle BOTH separators \u2014 a Windows-native path uses '\\\\'; a '/'-only\n // search would miss it and fall back to a bare relative path written\n // into the process CWD instead of the sessions dir.\n const lastSep = Math.max(sessionTaskPath.lastIndexOf('/'), sessionTaskPath.lastIndexOf('\\\\'));\n taskPath = lastSep >= 0\n ? sessionTaskPath.slice(0, lastSep + 1) + 'backlog.tasks.json'\n : 'backlog.tasks.json';\n }\n } else {\n taskPath = sessionTaskPath;\n }\n\n if (typeof taskPath !== 'string' || !taskPath) {\n return { ok: false, message: 'Task storage path not configured.', count: 0, completed: 0, inProgress: 0 };\n }\n const sessionId = ctx.session?.id ?? 'unknown';\n\n // Early-return result for validation errors that happen before or\n // during the critical section. The lock callback sets this instead of\n // mutating the file, and we return it after the lock releases.\n let early: TaskOutput | null = null;\n // Track promote output for the custom message\n const promoteMeta = { count: 0, title: '' };\n // Track planify data \u2014 written to plan file after the task lock releases\n const planifyMeta = { title: '', details: '' };\n let didPlanify = false;\n // collect todos to replace \u2014 called AFTER mutateTasks so rollback is possible\n type TodosReplacement = Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm?: string; promotedFromTask?: string }>;\n let todosToReplace: TodosReplacement | null = null;\n\n let file: TaskFile;\n try {\n file = await mutateTasks(taskPath, sessionId, async (f: TaskFile) => {\n switch (input.action) {\n case 'show':\n // read-only \u2014 no mutation, just return current state\n break;\n\n case 'replace': {\n if (!Array.isArray(input.tasks)) {\n early = { ok: false, message: 'action=replace requires `tasks` array.', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n // Validate id uniqueness: findTaskIndex / status resolve a task by\n // the FIRST id match, so a duplicate id silently becomes unaddressable.\n const newIds = new Set(input.tasks.map((t) => t.id));\n if (newIds.size !== input.tasks.length) {\n const seen = new Set<string>();\n const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => {\n if (seen.has(id)) return true;\n seen.add(id);\n return false;\n }))];\n early = {\n ok: false,\n message: `action=replace has duplicate task IDs: ${dupes.join(', ')}. Each task id must be unique.`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n return f;\n }\n // Validate dependsOn references: must point to IDs within the new batch\n for (const t of input.tasks) {\n if (t.dependsOn && t.dependsOn.length > 0) {\n const missing = t.dependsOn.filter((d) => !newIds.has(d));\n if (missing.length > 0) {\n early = {\n ok: false,\n message: `dependsOn validation failed: task \"${t.id}\" references unknown IDs: ${missing.join(', ')}`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n return f;\n }\n }\n }\n const now = new Date().toISOString();\n f.tasks = input.tasks.map((t) => ({\n ...t,\n createdAt: t.createdAt || now,\n updatedAt: now,\n }));\n break;\n }\n\n case 'add': {\n const t = input.task;\n if (!t?.title) {\n early = { ok: false, message: 'action=add requires `task` with at least `title`.', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n // Validate dependsOn: all referenced IDs must exist in the current task list\n if (t.dependsOn && t.dependsOn.length > 0) {\n const existingIds = new Set(f.tasks.map((e: TaskItem) => e.id));\n const missing = t.dependsOn.filter((d) => !existingIds.has(d));\n if (missing.length > 0) {\n early = {\n ok: false,\n message: `dependsOn validation failed: unknown task IDs: ${missing.join(', ')}`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n return f;\n }\n }\n const now = new Date().toISOString();\n const newTask: TaskItem = {\n id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,\n title: t.title,\n description: t.description,\n type: t.type || 'feature',\n priority: t.priority || 'medium',\n status: t.status || 'pending',\n dependsOn: t.dependsOn,\n assignee: t.assignee,\n estimateHours: t.estimateHours,\n tags: t.tags,\n createdAt: now,\n updatedAt: now,\n };\n f.tasks.push(newTask);\n break;\n }\n\n case 'status': {\n if (!input.id || !input.status) {\n early = { ok: false, message: 'action=status requires `id` and `status`.', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const task = f.tasks.find((t: TaskItem) => t.id === input.id);\n if (!task) {\n early = { ok: false, message: `Task \"${input.id}\" not found.`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n task.status = input.status;\n task.updatedAt = new Date().toISOString();\n break;\n }\n\n case 'promote': {\n const target = input.target?.trim();\n if (!target) {\n early = { ok: false, message: 'action=promote requires `target` (task id, index, or title substring).', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const idx = findTaskIndex(f.tasks, target);\n if (idx === -1) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const match = f.tasks[idx];\n /* v8 ignore next 4 -- findTaskIndex returned a valid in-range idx, so match is always defined; defensive. */\n if (!match) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n\n // Mark task in_progress\n if (match.status !== 'completed' && match.status !== 'failed') {\n match.status = 'in_progress';\n match.updatedAt = new Date().toISOString();\n }\n\n // Build todo items\n const todos: Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm?: string; promotedFromTask?: string }> = [];\n const ts = Date.now();\n todos.push({\n id: `todo_${ts}_task`,\n content: match.title,\n status: 'in_progress',\n activeForm: match.title,\n promotedFromTask: match.id,\n });\n\n if (match.description) {\n todos.push({\n id: `todo_${ts}_${randomUUID().slice(0, 6)}`,\n content: match.description.slice(0, 200),\n status: 'pending',\n promotedFromTask: match.id,\n });\n }\n\n if (input.subtasks && input.subtasks.length > 0) {\n for (const st of input.subtasks) {\n todos.push({\n id: `todo_${ts}_${randomUUID().slice(0, 6)}`,\n content: st,\n status: 'pending',\n promotedFromTask: match.id,\n });\n }\n }\n\n todosToReplace = todos;\n promoteMeta.count = todos.length;\n promoteMeta.title = match.title;\n break;\n }\n\n case 'planify': {\n const target = input.target?.trim();\n if (!target) {\n early = { ok: false, message: 'action=planify requires `target` (task id, index, or title substring).', count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const idx = findTaskIndex(f.tasks, target);\n if (idx === -1) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n const match = f.tasks[idx];\n /* v8 ignore next 4 -- findTaskIndex returned a valid in-range idx, so match is always defined; defensive. */\n if (!match) {\n early = { ok: false, message: `No task matched \"${target}\".`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n // Extract data \u2014 plan write happens after the task lock releases\n planifyMeta.title = match.title;\n planifyMeta.details = match.description ?? '';\n didPlanify = true;\n // Do NOT mutate the task \u2014 just copy to plan\n break;\n }\n\n default:\n early = { ok: false, message: `Unknown action \"${(input as { action: string }).action}\". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };\n return f;\n }\n\n return f;\n });\n } catch (err) {\n // Persist failed (mutateTasks throws on a failed save) \u2014 report ok:false\n // instead of falsely claiming the tasks were saved.\n return {\n ok: false,\n message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,\n count: 0,\n completed: 0,\n inProgress: 0,\n };\n }\n\n // Apply todo replacements after the task file mutation succeeds so that\n // on error the state is rolled back cleanly.\n if (todosToReplace) ctx.state.replaceTodos(todosToReplace);\n\n // A successful task mutation includes its Kanban projection. Awaiting this\n // keeps the session board authoritative at tool-return time.\n await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);\n\n // If the callback set an early-return result, use it\n if (early) return early;\n\n // If planify copied task data, write it to the plan file now\n if (didPlanify) {\n const { title, details } = planifyMeta;\n const planPathRaw = (ctx.meta as Record<string, unknown>)['plan.path'];\n const prog = computeTaskItemProgress(file.tasks);\n if (typeof planPathRaw === 'string' && planPathRaw) {\n let planPath: string = planPathRaw;\n // Honor project scope for the PLAN file too (mirror of plan.ts taskify);\n // handle both separators.\n if (input.scope === 'project') {\n const lastSep = Math.max(planPath.lastIndexOf('/'), planPath.lastIndexOf('\\\\'));\n planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + 'backlog.plan.json' : 'backlog.plan.json';\n }\n // Mutate the cross-file under ITS OWN lock so a concurrent plan tool\n // call in the same batch can't clobber the write.\n let formatted = '';\n try {\n await mutatePlan(planPath, sessionId, (pf) => {\n const { plan: updated } = addPlanItem(pf, title, details || undefined);\n formatted = formatPlan(updated);\n return updated;\n });\n } catch (err) {\n return {\n ok: false,\n message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,\n count: file.tasks.length,\n completed: prog.completed,\n inProgress: prog.inProgress,\n };\n }\n return {\n ok: true,\n message: `planify ok \u2014 added \"${title}\" to plan.\\n${formatted}`,\n count: file.tasks.length,\n completed: prog.completed,\n inProgress: prog.inProgress,\n };\n }\n // Plan path missing \u2014 still report the REAL task counts (the task file was\n // loaded and may be non-empty), not zeros.\n return {\n ok: false,\n message: 'Plan storage path not configured \u2014 cannot planify.',\n count: file.tasks.length,\n completed: prog.completed,\n inProgress: prog.inProgress,\n };\n }\n\n const p = computeTaskItemProgress(file.tasks);\n const summary = promoteMeta.count > 0\n ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from \"${promoteMeta.title}\".\\n${formatTaskList(file.tasks)}`\n : file.tasks.length > 0\n ? formatTaskList(file.tasks)\n : 'No tasks.';\n return {\n ok: true,\n message: summary,\n count: file.tasks.length,\n completed: p.completed,\n inProgress: p.inProgress,\n };\n },\n};\n", "import { type FSWatcher, watch } from 'node:fs';\nimport { basename, dirname } from 'node:path';\nimport type { Context, TodoItem } from '@wrongstack/core/agent';\nimport { GlobalMailbox } from '@wrongstack/core/coordination';\nimport {\n loadPlan,\n loadTasks,\n mutatePlan,\n mutateTasks,\n type PlanFile,\n type PlanItem,\n type TaskFile,\n} from '@wrongstack/core/storage';\nimport { deserializeTaskGraph } from '@wrongstack/core/tasking';\nimport type { SerializedTaskGraph, TaskStatus } from '@wrongstack/core/types';\nimport { resolveWstackPaths, type TaskItem } from '@wrongstack/core/utils';\nimport {\n createBoard,\n getBoard,\n getKanbanDir,\n type KanbanBoard,\n type KanbanColumn,\n type KanbanTask,\n listBoards,\n removeBoard,\n syncBoardFromTaskGraph,\n touchKanbanPresence,\n updateBoard,\n} from '@wrongstack/kanban';\n\nconst SESSION_BOARD_TAG = 'session-work';\nconst MIRROR_DISABLED_ENV = 'WRONGSTACK_KANBAN_TASK_MIRROR';\n\n/** The canonical workflow shared by WebUI and TUI session boards. */\nexport const SESSION_KANBAN_COLUMNS: KanbanColumn[] = [\n { id: 'todo', title: 'Todo', order: 0, wipLimit: 0, color: '#2563eb' },\n { id: 'in-progress', title: 'Running', order: 1, wipLimit: 1, color: '#d97706' },\n { id: 'review', title: 'Preview', order: 2, wipLimit: 0, color: '#7c3aed' },\n { id: 'done', title: 'Done', order: 3, wipLimit: 0, color: '#16a34a' },\n];\n\nconst boardQueue = new Map<string, Promise<void>>();\nconst boardEnsures = new Map<string, Promise<KanbanBoard>>();\ntype PendingMirror = {\n projectRoot: string;\n sessionId: string;\n graph: SerializedTaskGraph;\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan';\n};\nconst pendingMirrors = new Map<string, PendingMirror>();\nconst activeMirrors = new Set<string>();\nconst bindings = new WeakMap<Context, () => void>();\nconst suppressedTodoMirrors = new WeakSet<Context>();\nconst activeSessionBoards = new Map<string, number>();\n\nfunction boardKey(projectRoot: string, sessionId: string): string {\n return `${projectRoot}\\0${sessionId}`;\n}\n\nfunction mirrorKey(\n projectRoot: string,\n sessionId: string,\n sourceSystem: PendingMirror['sourceSystem'],\n): string {\n return `${boardKey(projectRoot, sessionId)}\\0${sourceSystem}`;\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction sessionBoardTitle(sessionId: string): string {\n const leaf = sessionId.split(/[\\\\/]/).filter(Boolean).pop() ?? sessionId;\n return `Session ${leaf.slice(0, 12)}`;\n}\n\nfunction sessionBoardTags(sessionId: string): string[] {\n return ['session', SESSION_BOARD_TAG, sessionTag(sessionId)];\n}\n\nfunction sessionIdFromTags(tags: readonly string[] | undefined): string | null {\n const tag = tags?.find((candidate) => candidate.startsWith('session:'));\n return tag?.slice('session:'.length) || null;\n}\n\nfunction isOwnedSessionBoard(tags: readonly string[] | undefined): boolean {\n return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));\n}\n\nfunction retainActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);\n}\n\nfunction releaseActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n const remaining = (activeSessionBoards.get(key) ?? 0) - 1;\n if (remaining > 0) activeSessionBoards.set(key, remaining);\n else activeSessionBoards.delete(key);\n}\n\nfunction isSessionBoardActive(projectRoot: string, sessionId: string): boolean {\n return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;\n}\n\nfunction sameColumns(columns: readonly KanbanColumn[]): boolean {\n return (\n columns.length === SESSION_KANBAN_COLUMNS.length &&\n columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id)\n );\n}\n\n/** Create (or migrate) the single Kanban board owned by a session. */\nexport async function ensureSessionKanbanBoard(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n const key = boardKey(projectRoot, sessionId);\n const inFlight = boardEnsures.get(key);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n const summary = (await listBoards(projectRoot)).find((board) =>\n board.tags?.includes(sessionTag(sessionId)),\n );\n let board = summary ? await getBoard(projectRoot, summary.id) : null;\n if (!board) {\n return createBoard(projectRoot, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: sessionBoardTags(sessionId),\n columns: SESSION_KANBAN_COLUMNS,\n generatedBy: `session-kanban:${sessionId}`,\n });\n }\n\n // Boards produced by the older task/plan mirrors used the generic five\n // columns. Migrate only session-owned boards; updateBoard reconciles cards\n // from Backlog into Todo while preserving Running/Preview/Done cards.\n if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {\n board =\n (await updateBoard(projectRoot, board.id, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n columns: SESSION_KANBAN_COLUMNS,\n })) ?? board;\n }\n return board;\n })();\n\n boardEnsures.set(key, promise);\n try {\n return await promise;\n } finally {\n boardEnsures.delete(key);\n }\n}\n\nfunction enqueueBoardWork<T>(\n projectRoot: string,\n sessionId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const key = boardKey(projectRoot, sessionId);\n const previous = boardQueue.get(key) ?? Promise.resolve();\n const result = previous.catch(() => undefined).then(work);\n const tail = result.then(\n () => undefined,\n () => undefined,\n );\n boardQueue.set(key, tail);\n void tail.then(() => {\n if (boardQueue.get(key) === tail) boardQueue.delete(key);\n });\n return result;\n}\n\nasync function removeEmptySessionBoard(\n projectRoot: string,\n boardId: string,\n sessionId: string,\n): Promise<string | null> {\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n if (isSessionBoardActive(projectRoot, sessionId)) return null;\n const board = await getBoard(projectRoot, boardId);\n if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;\n if (sessionIdFromTags(board.tags) !== sessionId) return null;\n return (await removeBoard(projectRoot, board.id)) ? board.id : null;\n });\n}\n\n/** Remove a particular inactive session's system-owned board when it has no cards. */\nexport async function cleanupSessionKanbanBoardIfEmpty(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<string[]> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n if (isSessionBoardActive(projectRoot, sessionId)) return [];\n const candidates = (await listBoards(projectRoot)).filter(\n (board) =>\n board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n sessionIdFromTags(board.tags) === sessionId,\n );\n const removed = await Promise.all(\n candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId)),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\n/** Prune stale empty session boards while preserving manual and live boards. */\nexport async function cleanupEmptySessionKanbanBoards(\n projectRoot: string | undefined,\n activeSessionId = '',\n): Promise<string[]> {\n if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n const candidates = (await listBoards(projectRoot)).flatMap((board) => {\n const ownerSessionId = sessionIdFromTags(board.tags);\n return board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n ownerSessionId &&\n ownerSessionId !== activeSessionId &&\n !isSessionBoardActive(projectRoot, ownerSessionId)\n ? [{ boardId: board.id, sessionId: ownerSessionId }]\n : [];\n });\n const removed = await Promise.all(\n candidates.map(({ boardId, sessionId }) =>\n removeEmptySessionBoard(projectRoot, boardId, sessionId),\n ),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\nasync function projectGraph(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan',\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n const board = await ensureSessionKanbanBoard(projectRoot, sessionId);\n if (!board) return null;\n const result = await syncBoardFromTaskGraph(\n projectRoot,\n board.id,\n deserializeTaskGraph(graph),\n {\n sourceSystem,\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n archiveMissingTasks: true,\n includeCompletedTasks: true,\n },\n );\n return result?.board ?? null;\n });\n}\n\n/**\n * Queue an observational mirror without retaining every intermediate state.\n * Todo/task/plan streams are independent, but within each stream only the\n * newest pending graph matters. This bounds a stalled file lock to one active\n * and one pending projection instead of an arbitrarily long Promise chain.\n */\nfunction queueLatestMirror(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: PendingMirror['sourceSystem'],\n): void {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return;\n const key = mirrorKey(projectRoot, sessionId, sourceSystem);\n pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });\n if (activeMirrors.has(key)) return;\n activeMirrors.add(key);\n void (async () => {\n try {\n for (;;) {\n const pending = pendingMirrors.get(key);\n if (!pending) break;\n pendingMirrors.delete(key);\n try {\n await projectGraph(\n pending.projectRoot,\n pending.sessionId,\n pending.graph,\n pending.sourceSystem,\n );\n } catch {\n // Mirrors are observational. A newer pending snapshot, if present,\n // still gets a chance after a transient lock or filesystem failure.\n }\n }\n } finally {\n activeMirrors.delete(key);\n // A mirror can arrive between the last Map read and deleting the active\n // marker. Re-arm once so that snapshot cannot be stranded.\n const pending = pendingMirrors.get(key);\n if (pending) {\n pendingMirrors.delete(key);\n queueLatestMirror(\n pending.projectRoot,\n pending.sessionId,\n pending.graph,\n pending.sourceSystem,\n );\n }\n }\n })();\n}\n\nexport function todoListToSerializedGraph(\n todos: readonly TodoItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = todos.map((todo, index) => ({\n id: todo.id,\n title: todo.content,\n description: todo.activeForm ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: todo.status,\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `todo:${sessionId}`,\n specId: `todo:${sessionId}`,\n title: 'Session todos',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function taskFileToSerializedGraph(\n tasks: readonly TaskItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const ids = new Set(tasks.map((task) => task.id));\n const nodes = tasks.map((task, index) => ({\n id: task.id,\n title: task.title,\n description: task.description ?? '',\n type: task.type,\n priority: task.priority,\n status: task.status,\n ...(task.assignee ? { assignee: task.assignee } : {}),\n ...(task.estimateHours !== undefined ? { estimateHours: task.estimateHours } : {}),\n createdAt: index,\n updatedAt: index,\n }));\n const edges = tasks.flatMap((task) =>\n (task.dependsOn ?? [])\n .filter((dependency) => ids.has(dependency))\n .map((dependency) => ({\n id: `${dependency}->${task.id}`,\n from: dependency,\n to: task.id,\n type: 'depends_on' as const,\n })),\n );\n const hasIncoming = new Set(edges.map((edge) => edge.to));\n const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);\n return {\n // Keep the historical graph id so existing mirrored task cards are reused.\n id: `session:${sessionId}`,\n specId: `session:${sessionId}`,\n title: 'Session tasks',\n nodes,\n edges,\n rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nconst PLAN_STATUS_TO_TASK: Record<PlanItem['status'], TaskStatus> = {\n open: 'pending',\n in_progress: 'in_progress',\n done: 'completed',\n};\n\nexport function planFileToSerializedGraph(\n items: readonly PlanItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = items.map((item, index) => ({\n id: item.id,\n title: item.title,\n description: item.details ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: PLAN_STATUS_TO_TASK[item.status],\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `plan:${sessionId}`,\n specId: `plan:${sessionId}`,\n title: 'Session plan',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function projectSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function projectSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function projectSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\nfunction fireAndForget(context: string, work: Promise<unknown>): void {\n void work.catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(JSON.stringify({ level: 'warn', event: 'session-kanban', context, message, timestamp: new Date().toISOString() }));\n });\n}\n\nfunction broadcastTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const sessionId = context.session?.id ?? '';\n if (!context.agentId || !sessionId) return;\n const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;\n const mailbox = new GlobalMailbox(projectDir);\n void mailbox\n .send({\n from: context.agentId,\n to: '*',\n type: 'status',\n subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? '' : 's'})`,\n body: JSON.stringify({\n kind: 'kanban.todos.updated',\n sessionId,\n revision: context.state.revision,\n todos,\n }),\n priority: 'normal',\n senderSessionId: sessionId,\n ttlMs: 6 * 60 * 60 * 1000,\n })\n .catch(() => {\n // Mailbox awareness is best-effort; canonical state is already updated.\n });\n}\n\nfunction notifyTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const summary = todos.length\n ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join('\\n')\n : '- No active todos remain.';\n const text = `[KANBAN TODO UPDATE]\\nAnother Kanban agent reassessed the shared board. The canonical todo list is now:\\n${summary}\\nReassess your current plan before continuing; do not rely on the initial todo snapshot.`;\n const state = context.state as Partial<Context['state']>;\n if (typeof state.appendBlockToLastUserMessage === 'function') {\n if (state.appendBlockToLastUserMessage({ type: 'text', text })) return;\n }\n if (typeof state.appendMessage === 'function') {\n state.appendMessage({ role: 'user', content: [{ type: 'text', text }] });\n }\n}\n\nexport function mirrorSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function mirrorSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function mirrorSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): void {\n queueLatestMirror(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\n/**\n * Bind all live session work paths to Kanban. Todo changes are observed from\n * ConversationState; plan/task sidecars are watched so slash commands, WebUI,\n * plugins, and tools all pass through the same board.\n */\nexport function attachSessionKanbanMirror(context: Context): () => void {\n const existing = bindings.get(context);\n if (existing) return existing;\n\n const attachedProjectRoot = context.projectRoot ?? '';\n let registeredSessionId = '';\n const syncActiveSessionRegistration = () => {\n if (!attachedProjectRoot) return;\n const currentSessionId = context.session?.id ?? '';\n if (currentSessionId === registeredSessionId) return;\n if (registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(\n 'cleanup-board',\n cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId),\n );\n }\n registeredSessionId = currentSessionId;\n if (registeredSessionId) {\n retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n }\n };\n syncActiveSessionRegistration();\n\n let watcher: FSWatcher | null = null;\n let watchedDir = '';\n let timer: NodeJS.Timeout | null = null;\n let boardWatcher: FSWatcher | null = null;\n let watchedBoardId = '';\n let boardTimer: NodeJS.Timeout | null = null;\n let presenceTimer: NodeJS.Timeout | null = null;\n\n const sessionId = () => context.session?.id ?? '';\n const refreshFiles = async () => {\n const id = sessionId();\n if (!id) return;\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n };\n\n const refreshBoard = async () => {\n if (!watchedBoardId) return;\n const board = await getBoard(context.projectRoot, watchedBoardId);\n if (board) applySessionKanbanBoardToTodos(context, board);\n };\n\n const configureBoardWatcher = async () => {\n const id = sessionId();\n const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;\n if (!board || board.id === watchedBoardId) return;\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = board.id;\n try {\n const boardFileName = `${board.id}.json`;\n boardWatcher = watch(\n getKanbanDir(context.projectRoot),\n { persistent: false },\n (_event, filename) => {\n if (filename?.toString() !== boardFileName) return;\n if (boardTimer) clearTimeout(boardTimer);\n boardTimer = setTimeout(() => fireAndForget('refresh-board', refreshBoard()), 60);\n },\n );\n const touchPresence = () =>\n touchKanbanPresence(context.projectRoot, board.id, {\n sessionId: id,\n agentId: context.agentId,\n agentName: context.agentName,\n });\n fireAndForget('touch-presence', touchPresence());\n if (presenceTimer) clearInterval(presenceTimer);\n presenceTimer = setInterval(() => fireAndForget('touch-presence', touchPresence()), 60_000);\n presenceTimer.unref?.();\n boardWatcher.on('error', () => {\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = '';\n });\n } catch {\n boardWatcher = null;\n watchedBoardId = '';\n }\n };\n\n const configureWatcher = () => {\n const planPath = context.meta['plan.path'];\n const taskPath = context.meta['task.path'];\n const candidate =\n typeof planPath === 'string' && planPath\n ? dirname(planPath)\n : typeof taskPath === 'string' && taskPath\n ? dirname(taskPath)\n : '';\n if (!candidate || candidate === watchedDir) return;\n watcher?.close();\n watcher = null;\n watchedDir = candidate;\n try {\n watcher = watch(candidate, { persistent: false }, (_event, filename) => {\n const name = filename?.toString();\n const currentPlanPath = context.meta['plan.path'];\n const currentTaskPath = context.meta['task.path'];\n const planName = typeof currentPlanPath === 'string' ? basename(currentPlanPath) : '';\n const taskName = typeof currentTaskPath === 'string' ? basename(currentTaskPath) : '';\n if (name && name !== planName && name !== taskName) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fireAndForget('refresh-files', refreshFiles()), 60);\n });\n watcher.on('error', () => watcher?.close());\n } catch {\n // The session directory can briefly disappear during project/session\n // switches; the next meta update re-attempts the binding.\n watcher = null;\n watchedDir = '';\n }\n };\n\n const unsubscribe = context.state.onChange((change) => {\n if (change.kind === 'todos_replaced' && !suppressedTodoMirrors.has(context)) {\n // ConversationState auto-clears an all-done tactical list. Project the\n // pre-clear completion snapshot so every card reaches Done atomically.\n mirrorSessionTodosToKanban(\n context.projectRoot,\n change.completedSnapshot ?? change.todos,\n sessionId(),\n );\n return;\n }\n if (change.kind === 'meta_set' && (change.key === 'plan.path' || change.key === 'task.path')) {\n syncActiveSessionRegistration();\n configureWatcher();\n fireAndForget('ensure-board', ensureSessionKanbanBoard(context.projectRoot, sessionId()));\n fireAndForget('configure-watcher', configureBoardWatcher());\n fireAndForget('refresh-files', refreshFiles());\n }\n });\n\n configureWatcher();\n fireAndForget('configure-watcher', configureBoardWatcher());\n\n const detach = () => {\n unsubscribe();\n if (timer) clearTimeout(timer);\n if (boardTimer) clearTimeout(boardTimer);\n if (presenceTimer) clearInterval(presenceTimer);\n watcher?.close();\n boardWatcher?.close();\n bindings.delete(context);\n if (attachedProjectRoot && registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(\n 'cleanup-board',\n cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId),\n );\n registeredSessionId = '';\n }\n };\n bindings.set(context, detach);\n return detach;\n}\n\n/** Fully hydrate a session board before a host announces the session as ready. */\nexport async function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null> {\n const id = context.session?.id ?? '';\n if (!id) return null;\n await cleanupEmptySessionKanbanBoards(context.projectRoot, id);\n let board = await ensureSessionKanbanBoard(context.projectRoot, id);\n if (context.todos.length) {\n board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);\n }\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n return board;\n}\n\nexport interface SessionKanbanSourceUpdate {\n source: 'todo' | 'task' | 'plan' | null;\n todos?: TodoItem[] | undefined;\n tasks?: TaskFile | undefined;\n plan?: PlanFile | undefined;\n}\n\nfunction sourceStatus(task: KanbanTask): TaskStatus {\n if (task.status === 'completed') return 'completed';\n if (task.status === 'in_progress') return 'in_progress';\n if (task.status === 'review') return 'review';\n if (task.status === 'blocked') return 'blocked';\n if (task.status === 'failed') return 'failed';\n return 'pending';\n}\n\nfunction todoStatus(task: KanbanTask): TodoItem['status'] {\n const status = sourceStatus(task);\n if (status === 'completed') return 'completed';\n if (status === 'in_progress' || status === 'review') return 'in_progress';\n return 'pending';\n}\n\nfunction sessionTodoFromTask(task: KanbanTask): TodoItem {\n return {\n id: task.origin?.taskId ?? task.id,\n content: task.title,\n status: todoStatus(task),\n ...(task.description ? { activeForm: task.description } : {}),\n };\n}\n\nfunction sameTodos(left: readonly TodoItem[], right: readonly TodoItem[]): boolean {\n return (\n left.length === right.length &&\n left.every((todo, index) => {\n const candidate = right[index];\n return (\n candidate?.id === todo.id &&\n candidate.content === todo.content &&\n candidate.status === todo.status &&\n candidate.activeForm === todo.activeForm &&\n candidate.promotedFromPlan === todo.promotedFromPlan &&\n candidate.promotedFromTask === todo.promotedFromTask\n );\n })\n );\n}\n\n/**\n * Replace the tactical todo list from the current cards on a session-owned board.\n *\n * Existing mirrored todo cards retain their stable source ids. New cards created\n * by a Kanban worker become todos under their card ids, so reassessment can add,\n * split, merge, reprioritize, or remove work without being overwritten by the\n * todo list that happened to exist when the run started.\n */\nexport function applySessionKanbanBoardToTodos(context: Context, board: KanbanBoard): TodoItem[] {\n const sessionId = context.session?.id ?? '';\n if (\n !sessionId ||\n sessionIdFromTags(board.tags) !== sessionId ||\n !isOwnedSessionBoard(board.tags)\n ) {\n return [...context.todos];\n }\n\n const projectedTodos = board.tasks\n .filter(\n (task) =>\n task.status !== 'archived' &&\n (!task.origin ||\n task.origin.system === 'session-todo' ||\n (task.origin.graphId ?? '').startsWith('todo:')),\n )\n .sort((left, right) => {\n const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;\n const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;\n return (\n leftColumn - rightColumn ||\n left.order - right.order ||\n left.createdAt.localeCompare(right.createdAt)\n );\n })\n .map(sessionTodoFromTask);\n\n const allCompleted =\n projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === 'completed');\n const effectiveTodos = allCompleted ? [] : projectedTodos;\n if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(projectedTodos);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n notifyTodoUpdate(context, context.todos);\n broadcastTodoUpdate(context, context.todos);\n return [...context.todos];\n}\n\n/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */\nexport async function applySessionKanbanTaskToSource(\n context: Context,\n task: KanbanTask,\n options: { remove?: boolean | undefined } = {},\n): Promise<SessionKanbanSourceUpdate> {\n const originId = task.origin?.taskId;\n const graphId = task.origin?.graphId ?? '';\n if (!originId) return { source: null };\n\n if (task.origin?.system === 'session-todo' || graphId.startsWith('todo:')) {\n const next = options.remove\n ? context.todos.filter((todo) => todo.id !== originId)\n : context.todos.map((todo) =>\n todo.id === originId\n ? {\n ...todo,\n content: task.title,\n status:\n sourceStatus(task) === 'completed'\n ? ('completed' as const)\n : sourceStatus(task) === 'in_progress' || sourceStatus(task) === 'review'\n ? ('in_progress' as const)\n : ('pending' as const),\n }\n : todo,\n );\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(next);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n return { source: 'todo', todos: [...context.todos] };\n }\n\n const id = context.session?.id ?? '';\n if (task.origin?.system === 'session-plan' || graphId.startsWith('plan:')) {\n const planPath = context.meta['plan.path'];\n if (typeof planPath !== 'string' || !planPath) return { source: 'plan' };\n const plan = await mutatePlan(planPath, id, (file) => ({\n ...file,\n updatedAt: new Date().toISOString(),\n items: options.remove\n ? file.items.filter((item) => item.id !== originId)\n : file.items.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n details: task.description,\n status:\n task.status === 'completed'\n ? ('done' as const)\n : task.status === 'in_progress' || task.status === 'review'\n ? ('in_progress' as const)\n : ('open' as const),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'plan', plan };\n }\n\n if (\n task.origin?.system === 'session-task' ||\n task.origin?.system === 'session' ||\n graphId.startsWith('session:')\n ) {\n const taskPath = context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) return { source: 'task' };\n const tasks = await mutateTasks(taskPath, id, (file) => ({\n ...file,\n tasks: options.remove\n ? file.tasks.filter((item) => item.id !== originId)\n : file.tasks.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n description: task.description,\n status: sourceStatus(task),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'task', tasks };\n }\n\n return { source: null };\n}\n"],
|
|
5
|
+
"mappings": ";AAEA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,eAAAA,oBAAmB;AAC5B;AAAA,EACE;AAAA,EACA,cAAAC;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;;;ACV3B,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,4BAA4B;AAErC,SAAS,0BAAyC;AAClD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,IAAM,oBAAoB;AAC1B,IAAM,sBAAsB;AAGrB,IAAM,yBAAyC;AAAA,EACpD,EAAE,IAAI,QAAQ,OAAO,QAAQ,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AAAA,EACrE,EAAE,IAAI,eAAe,OAAO,WAAW,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AAAA,EAC/E,EAAE,IAAI,UAAU,OAAO,WAAW,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AAAA,EAC1E,EAAE,IAAI,QAAQ,OAAO,QAAQ,OAAO,GAAG,UAAU,GAAG,OAAO,UAAU;AACvE;AAEA,IAAM,aAAa,oBAAI,IAA2B;AAClD,IAAM,eAAe,oBAAI,IAAkC;AAa3D,SAAS,SAAS,aAAqB,WAA2B;AAChE,SAAO,GAAG,WAAW,KAAK,SAAS;AACrC;AAUA,SAAS,WAAW,WAA2B;AAC7C,SAAO,WAAW,SAAS;AAC7B;AAEA,SAAS,kBAAkB,WAA2B;AACpD,QAAM,OAAO,UAAU,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK;AAC/D,SAAO,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AACrC;AAEA,SAAS,iBAAiB,WAA6B;AACrD,SAAO,CAAC,WAAW,mBAAmB,WAAW,SAAS,CAAC;AAC7D;AA2BA,SAAS,YAAY,SAA2C;AAC9D,SACE,QAAQ,WAAW,uBAAuB,UAC1C,QAAQ,MAAM,CAAC,QAAQ,UAAU,OAAO,OAAO,uBAAuB,KAAK,GAAG,EAAE;AAEpF;AAGA,eAAsB,yBACpB,aACA,WAC6B;AAC7B,MAAI,CAAC,eAAe,CAAC,aAAa,QAAQ,IAAI,mBAAmB,MAAM,IAAK,QAAO;AACnF,QAAM,MAAM,SAAS,aAAa,SAAS;AAC3C,QAAM,WAAW,aAAa,IAAI,GAAG;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,WAAW,YAAY;AAC3B,UAAM,WAAW,MAAM,WAAW,WAAW,GAAG;AAAA,MAAK,CAACC,WACpDA,OAAM,MAAM,SAAS,WAAW,SAAS,CAAC;AAAA,IAC5C;AACA,QAAI,QAAQ,UAAU,MAAM,SAAS,aAAa,QAAQ,EAAE,IAAI;AAChE,QAAI,CAAC,OAAO;AACV,aAAO,YAAY,aAAa;AAAA,QAC9B,OAAO,kBAAkB,SAAS;AAAA,QAClC,aAAa;AAAA,QACb,MAAM,iBAAiB,SAAS;AAAA,QAChC,SAAS;AAAA,QACT,aAAa,kBAAkB,SAAS;AAAA,MAC1C,CAAC;AAAA,IACH;AAKA,QAAI,CAAC,YAAY,MAAM,OAAO,KAAK,CAAC,MAAM,MAAM,SAAS,iBAAiB,GAAG;AAC3E,cACG,MAAM,YAAY,aAAa,MAAM,IAAI;AAAA,QACxC,OAAO,kBAAkB,SAAS;AAAA,QAClC,aAAa;AAAA,QACb,MAAM,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,iBAAiB,SAAS,CAAC,CAAC,CAAC;AAAA,QAC1E,SAAS;AAAA,MACX,CAAC,KAAM;AAAA,IACX;AACA,WAAO;AAAA,EACT,GAAG;AAEH,eAAa,IAAI,KAAK,OAAO;AAC7B,MAAI;AACF,WAAO,MAAM;AAAA,EACf,UAAE;AACA,iBAAa,OAAO,GAAG;AAAA,EACzB;AACF;AAEA,SAAS,iBACP,aACA,WACA,MACY;AACZ,QAAM,MAAM,SAAS,aAAa,SAAS;AAC3C,QAAM,WAAW,WAAW,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACxD,QAAM,SAAS,SAAS,MAAM,MAAM,MAAS,EAAE,KAAK,IAAI;AACxD,QAAM,OAAO,OAAO;AAAA,IAClB,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACA,aAAW,IAAI,KAAK,IAAI;AACxB,OAAK,KAAK,KAAK,MAAM;AACnB,QAAI,WAAW,IAAI,GAAG,MAAM,KAAM,YAAW,OAAO,GAAG;AAAA,EACzD,CAAC;AACD,SAAO;AACT;AA2DA,eAAe,aACb,aACA,WACA,OACA,cAC6B;AAC7B,MAAI,CAAC,eAAe,CAAC,aAAa,QAAQ,IAAI,mBAAmB,MAAM,IAAK,QAAO;AACnF,SAAO,iBAAiB,aAAa,WAAW,YAAY;AAC1D,UAAM,QAAQ,MAAM,yBAAyB,aAAa,SAAS;AACnE,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA,MAAM;AAAA,MACN,qBAAqB,KAAK;AAAA,MAC1B;AAAA,QACE;AAAA,QACA,MAAM,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,iBAAiB,SAAS,CAAC,CAAC,CAAC;AAAA,QAC1E,qBAAqB;AAAA,QACrB,uBAAuB;AAAA,MACzB;AAAA,IACF;AACA,WAAO,QAAQ,SAAS;AAAA,EAC1B,CAAC;AACH;AAiFO,SAAS,0BACd,OACA,WACqB;AACrB,QAAM,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAChD,QAAM,QAAQ,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,IACxC,IAAI,KAAK;AAAA,IACT,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK,eAAe;AAAA,IACjC,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,IACf,QAAQ,KAAK;AAAA,IACb,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;AAAA,IACnD,GAAI,KAAK,kBAAkB,SAAY,EAAE,eAAe,KAAK,cAAc,IAAI,CAAC;AAAA,IAChF,WAAW;AAAA,IACX,WAAW;AAAA,EACb,EAAE;AACF,QAAM,QAAQ,MAAM;AAAA,IAAQ,CAAC,UAC1B,KAAK,aAAa,CAAC,GACjB,OAAO,CAAC,eAAe,IAAI,IAAI,UAAU,CAAC,EAC1C,IAAI,CAAC,gBAAgB;AAAA,MACpB,IAAI,GAAG,UAAU,KAAK,KAAK,EAAE;AAAA,MAC7B,MAAM;AAAA,MACN,IAAI,KAAK;AAAA,MACT,MAAM;AAAA,IACR,EAAE;AAAA,EACN;AACA,QAAM,cAAc,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AACxD,QAAM,YAAY,MAAM,OAAO,CAAC,SAAS,CAAC,YAAY,IAAI,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,EAAE;AACzF,SAAO;AAAA;AAAA,IAEL,IAAI,WAAW,SAAS;AAAA,IACxB,QAAQ,WAAW,SAAS;AAAA,IAC5B,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,WAAW,UAAU,SAAS,YAAY,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC;AAAA,IACtE,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AA+CO,SAAS,4BACd,aACA,OACA,WAC6B;AAC7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,0BAA0B,OAAO,SAAS;AAAA,IAC1C;AAAA,EACF;AACF;;;ADlZA,SAAS,cAAc,OAAmB,OAAuB;AAC/D,QAAM,QAAQ,OAAO,SAAS,OAAO,EAAE;AACvC,MAAI,CAAC,OAAO,MAAM,KAAK,GAAG;AACxB,UAAM,MAAM,QAAQ;AACpB,QAAI,MAAM,GAAG,EAAG,QAAO;AAAA,EACzB;AACA,QAAM,OAAO,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO,KAAK;AAClD,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,QAAQ,MAAM,YAAY;AAChC,SAAO,MAAM,UAAU,CAAC,MAAM,EAAE,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC;AACrE;AAsCO,IAAM,WAAwC;AAAA,EACnD,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aACE;AAAA,EAKF,WACE;AAAA,EAcF,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,cAAc,CAAC,UAAU;AAAA,EACzB,MAAM;AAAA,EACN,WAAW;AAAA,EACX,aAAa;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,MACV,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM,CAAC,WAAW,OAAO,UAAU,QAAQ,WAAW,SAAS;AAAA,QAC/D,aAAa;AAAA,MACf;AAAA,MACA,OAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,UACL,MAAM;AAAA,UACN,YAAY;AAAA,YACV,IAAI,EAAE,MAAM,UAAU,aAAa,sCAAsC;AAAA,YACzE,OAAO,EAAE,MAAM,UAAU,aAAa,eAAe;AAAA,YACrD,aAAa,EAAE,MAAM,UAAU,aAAa,oBAAoB;AAAA,YAChE,MAAM,EAAE,MAAM,UAAU,MAAM,CAAC,WAAW,UAAU,YAAY,QAAQ,QAAQ,OAAO,EAAE;AAAA,YACzF,UAAU,EAAE,MAAM,UAAU,MAAM,CAAC,YAAY,QAAQ,UAAU,KAAK,EAAE;AAAA,YACxE,QAAQ,EAAE,MAAM,UAAU,MAAM,CAAC,WAAW,eAAe,WAAW,UAAU,UAAU,WAAW,EAAE;AAAA,YACvG,WAAW;AAAA,cACT,MAAM;AAAA,cACN,OAAO,EAAE,MAAM,SAAS;AAAA,cACxB,aAAa;AAAA,YACf;AAAA,YACA,UAAU,EAAE,MAAM,UAAU,aAAa,2BAA2B;AAAA,YACpE,eAAe,EAAE,MAAM,UAAU,aAAa,mBAAmB;AAAA,YACjE,MAAM,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,GAAG,aAAa,iBAAiB;AAAA,YAChF,WAAW,EAAE,MAAM,SAAS;AAAA,YAC5B,WAAW,EAAE,MAAM,SAAS;AAAA,UAC9B;AAAA,UACA,UAAU,CAAC,MAAM,SAAS,QAAQ,YAAY,QAAQ;AAAA,QACxD;AAAA,QACA,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,YAAY;AAAA,UACV,OAAO,EAAE,MAAM,SAAS;AAAA,UACxB,aAAa,EAAE,MAAM,SAAS;AAAA,UAC9B,MAAM,EAAE,MAAM,UAAU,MAAM,CAAC,WAAW,UAAU,YAAY,QAAQ,QAAQ,OAAO,EAAE;AAAA,UACzF,UAAU,EAAE,MAAM,UAAU,MAAM,CAAC,YAAY,QAAQ,UAAU,KAAK,EAAE;AAAA,UACxE,QAAQ,EAAE,MAAM,UAAU,MAAM,CAAC,WAAW,eAAe,WAAW,UAAU,UAAU,WAAW,EAAE;AAAA,UACvG,WAAW,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,UACtD,UAAU,EAAE,MAAM,SAAS;AAAA,UAC3B,eAAe,EAAE,MAAM,SAAS;AAAA,UAChC,MAAM,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,QACnD;AAAA,QACA,UAAU,CAAC,SAAS,QAAQ,UAAU;AAAA,QACtC,aAAa;AAAA,MACf;AAAA,MACA,IAAI,EAAE,MAAM,UAAU,aAAa,0DAA0D;AAAA,MAC7F,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,MAAM,CAAC,WAAW,eAAe,WAAW,UAAU,UAAU,WAAW;AAAA,QAC3E,aAAa;AAAA,MACf;AAAA,MACA,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO,EAAE,MAAM,SAAS;AAAA,QACxB,aAAa;AAAA,MACf;AAAA,MACA,OAAO;AAAA,QACL,MAAM;AAAA,QACN,MAAM,CAAC,WAAW,SAAS;AAAA,QAC3B,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,UAAU,CAAC,QAAQ;AAAA,EACrB;AAAA,EACA,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,kBAAmB,IAAI,KAAiC,WAAW;AACzE,QAAI;AAEJ,QAAI,MAAM,UAAU,WAAW;AAG7B,UAAI,OAAO,oBAAoB,UAAU;AAIvC,cAAM,UAAU,KAAK,IAAI,gBAAgB,YAAY,GAAG,GAAG,gBAAgB,YAAY,IAAI,CAAC;AAC5F,mBAAW,WAAW,IAClB,gBAAgB,MAAM,GAAG,UAAU,CAAC,IAAI,uBACxC;AAAA,MACN;AAAA,IACF,OAAO;AACL,iBAAW;AAAA,IACb;AAEA,QAAI,OAAO,aAAa,YAAY,CAAC,UAAU;AAC7C,aAAO,EAAE,IAAI,OAAO,SAAS,qCAAqC,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AAAA,IAC1G;AACA,UAAM,YAAY,IAAI,SAAS,MAAM;AAKrC,QAAI,QAA2B;AAE/B,UAAM,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG;AAE1C,UAAM,cAAc,EAAE,OAAO,IAAI,SAAS,GAAG;AAC7C,QAAI,aAAa;AAGjB,QAAI,iBAA0C;AAE9C,QAAI;AACJ,QAAI;AACJ,aAAO,MAAMC,aAAY,UAAU,WAAW,OAAO,MAAgB;AACnE,gBAAQ,MAAM,QAAQ;AAAA,UACpB,KAAK;AAEH;AAAA,UAEF,KAAK,WAAW;AACd,gBAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,GAAG;AAC/B,sBAAQ,EAAE,IAAI,OAAO,SAAS,0CAA0C,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AAC9G,qBAAO;AAAA,YACT;AAGA,kBAAM,SAAS,IAAI,IAAI,MAAM,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AACnD,gBAAI,OAAO,SAAS,MAAM,MAAM,QAAQ;AACtC,oBAAM,OAAO,oBAAI,IAAY;AAC7B,oBAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,MAAM,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,OAAO;AACpE,oBAAI,KAAK,IAAI,EAAE,EAAG,QAAO;AACzB,qBAAK,IAAI,EAAE;AACX,uBAAO;AAAA,cACT,CAAC,CAAC,CAAC;AACH,sBAAQ;AAAA,gBACN,IAAI;AAAA,gBACJ,SAAS,0CAA0C,MAAM,KAAK,IAAI,CAAC;AAAA,gBACnE,OAAO;AAAA,gBACP,WAAW;AAAA,gBACX,YAAY;AAAA,cACd;AACA,qBAAO;AAAA,YACT;AAEA,uBAAW,KAAK,MAAM,OAAO;AAC3B,kBAAI,EAAE,aAAa,EAAE,UAAU,SAAS,GAAG;AACzC,sBAAM,UAAU,EAAE,UAAU,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;AACxD,oBAAI,QAAQ,SAAS,GAAG;AACtB,0BAAQ;AAAA,oBACN,IAAI;AAAA,oBACJ,SAAS,sCAAsC,EAAE,EAAE,6BAA6B,QAAQ,KAAK,IAAI,CAAC;AAAA,oBAClG,OAAO;AAAA,oBACP,WAAW;AAAA,oBACX,YAAY;AAAA,kBACd;AACA,yBAAO;AAAA,gBACT;AAAA,cACF;AAAA,YACF;AACA,kBAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,cAAE,QAAQ,MAAM,MAAM,IAAI,CAAC,OAAO;AAAA,cAChC,GAAG;AAAA,cACH,WAAW,EAAE,aAAa;AAAA,cAC1B,WAAW;AAAA,YACb,EAAE;AACF;AAAA,UACF;AAAA,UAEA,KAAK,OAAO;AACV,kBAAM,IAAI,MAAM;AAChB,gBAAI,CAAC,GAAG,OAAO;AACb,sBAAQ,EAAE,IAAI,OAAO,SAAS,qDAAqD,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACzH,qBAAO;AAAA,YACT;AAEA,gBAAI,EAAE,aAAa,EAAE,UAAU,SAAS,GAAG;AACzC,oBAAM,cAAc,IAAI,IAAI,EAAE,MAAM,IAAI,CAAC,MAAgB,EAAE,EAAE,CAAC;AAC9D,oBAAM,UAAU,EAAE,UAAU,OAAO,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC;AAC7D,kBAAI,QAAQ,SAAS,GAAG;AACtB,wBAAQ;AAAA,kBACN,IAAI;AAAA,kBACJ,SAAS,kDAAkD,QAAQ,KAAK,IAAI,CAAC;AAAA,kBAC7E,OAAO;AAAA,kBACP,WAAW;AAAA,kBACX,YAAY;AAAA,gBACd;AACA,uBAAO;AAAA,cACT;AAAA,YACF;AACA,kBAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,kBAAM,UAAoB;AAAA,cACxB,IAAI,QAAQ,KAAK,IAAI,CAAC,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,cAClD,OAAO,EAAE;AAAA,cACT,aAAa,EAAE;AAAA,cACf,MAAM,EAAE,QAAQ;AAAA,cAChB,UAAU,EAAE,YAAY;AAAA,cACxB,QAAQ,EAAE,UAAU;AAAA,cACpB,WAAW,EAAE;AAAA,cACb,UAAU,EAAE;AAAA,cACZ,eAAe,EAAE;AAAA,cACjB,MAAM,EAAE;AAAA,cACR,WAAW;AAAA,cACX,WAAW;AAAA,YACb;AACA,cAAE,MAAM,KAAK,OAAO;AACpB;AAAA,UACF;AAAA,UAEA,KAAK,UAAU;AACb,gBAAI,CAAC,MAAM,MAAM,CAAC,MAAM,QAAQ;AAC9B,sBAAQ,EAAE,IAAI,OAAO,SAAS,6CAA6C,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACjH,qBAAO;AAAA,YACT;AACA,kBAAM,OAAO,EAAE,MAAM,KAAK,CAAC,MAAgB,EAAE,OAAO,MAAM,EAAE;AAC5D,gBAAI,CAAC,MAAM;AACT,sBAAQ,EAAE,IAAI,OAAO,SAAS,SAAS,MAAM,EAAE,gBAAgB,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACrG,qBAAO;AAAA,YACT;AACA,iBAAK,SAAS,MAAM;AACpB,iBAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC;AAAA,UACF;AAAA,UAEA,KAAK,WAAW;AACd,kBAAM,SAAS,MAAM,QAAQ,KAAK;AAClC,gBAAI,CAAC,QAAQ;AACX,sBAAQ,EAAE,IAAI,OAAO,SAAS,0EAA0E,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AAC9I,qBAAO;AAAA,YACT;AACA,kBAAM,MAAM,cAAc,EAAE,OAAO,MAAM;AACzC,gBAAI,QAAQ,IAAI;AACd,sBAAQ,EAAE,IAAI,OAAO,SAAS,oBAAoB,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACpG,qBAAO;AAAA,YACT;AACA,kBAAM,QAAQ,EAAE,MAAM,GAAG;AAEzB,gBAAI,CAAC,OAAO;AACV,sBAAQ,EAAE,IAAI,OAAO,SAAS,oBAAoB,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACpG,qBAAO;AAAA,YACT;AAGA,gBAAI,MAAM,WAAW,eAAe,MAAM,WAAW,UAAU;AAC7D,oBAAM,SAAS;AACf,oBAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,YAC3C;AAGA,kBAAM,QAAiJ,CAAC;AACxJ,kBAAM,KAAK,KAAK,IAAI;AACpB,kBAAM,KAAK;AAAA,cACT,IAAI,QAAQ,EAAE;AAAA,cACd,SAAS,MAAM;AAAA,cACf,QAAQ;AAAA,cACR,YAAY,MAAM;AAAA,cAClB,kBAAkB,MAAM;AAAA,YAC1B,CAAC;AAED,gBAAI,MAAM,aAAa;AACrB,oBAAM,KAAK;AAAA,gBACT,IAAI,QAAQ,EAAE,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,gBAC1C,SAAS,MAAM,YAAY,MAAM,GAAG,GAAG;AAAA,gBACvC,QAAQ;AAAA,gBACR,kBAAkB,MAAM;AAAA,cAC1B,CAAC;AAAA,YACH;AAEA,gBAAI,MAAM,YAAY,MAAM,SAAS,SAAS,GAAG;AAC/C,yBAAW,MAAM,MAAM,UAAU;AAC/B,sBAAM,KAAK;AAAA,kBACT,IAAI,QAAQ,EAAE,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,kBAC1C,SAAS;AAAA,kBACT,QAAQ;AAAA,kBACR,kBAAkB,MAAM;AAAA,gBAC1B,CAAC;AAAA,cACH;AAAA,YACF;AAEA,6BAAiB;AACjB,wBAAY,QAAQ,MAAM;AAC1B,wBAAY,QAAQ,MAAM;AAC1B;AAAA,UACF;AAAA,UAEA,KAAK,WAAW;AACd,kBAAM,SAAS,MAAM,QAAQ,KAAK;AAClC,gBAAI,CAAC,QAAQ;AACX,sBAAQ,EAAE,IAAI,OAAO,SAAS,0EAA0E,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AAC9I,qBAAO;AAAA,YACT;AACA,kBAAM,MAAM,cAAc,EAAE,OAAO,MAAM;AACzC,gBAAI,QAAQ,IAAI;AACd,sBAAQ,EAAE,IAAI,OAAO,SAAS,oBAAoB,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACpG,qBAAO;AAAA,YACT;AACA,kBAAM,QAAQ,EAAE,MAAM,GAAG;AAEzB,gBAAI,CAAC,OAAO;AACV,sBAAQ,EAAE,IAAI,OAAO,SAAS,oBAAoB,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACpG,qBAAO;AAAA,YACT;AAEA,wBAAY,QAAQ,MAAM;AAC1B,wBAAY,UAAU,MAAM,eAAe;AAC3C,yBAAa;AAEb;AAAA,UACF;AAAA,UAEA;AACE,oBAAQ,EAAE,IAAI,OAAO,SAAS,mBAAoB,MAA6B,MAAM,6DAA6D,OAAO,GAAG,WAAW,GAAG,YAAY,EAAE;AACxL,mBAAO;AAAA,QACX;AAEA,eAAO;AAAA,MACT,CAAC;AAAA,IACD,SAAS,KAAK;AAGZ,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,SAAS,gCAA2B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QACpF,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,MACd;AAAA,IACF;AAIA,QAAI,eAAgB,KAAI,MAAM,aAAa,cAAc;AAIzD,UAAM,4BAA4B,IAAI,aAAa,KAAK,OAAO,SAAS;AAGxE,QAAI,MAAO,QAAO;AAGlB,QAAI,YAAY;AACd,YAAM,EAAE,OAAO,QAAQ,IAAI;AAC3B,YAAM,cAAe,IAAI,KAAiC,WAAW;AACrE,YAAM,OAAO,wBAAwB,KAAK,KAAK;AAC/C,UAAI,OAAO,gBAAgB,YAAY,aAAa;AAClD,YAAI,WAAmB;AAGvB,YAAI,MAAM,UAAU,WAAW;AAC7B,gBAAM,UAAU,KAAK,IAAI,SAAS,YAAY,GAAG,GAAG,SAAS,YAAY,IAAI,CAAC;AAC9E,qBAAW,WAAW,IAAI,SAAS,MAAM,GAAG,UAAU,CAAC,IAAI,sBAAsB;AAAA,QACnF;AAGA,YAAI,YAAY;AAChB,YAAI;AACF,gBAAMC,YAAW,UAAU,WAAW,CAAC,OAAO;AAC5C,kBAAM,EAAE,MAAM,QAAQ,IAAI,YAAY,IAAI,OAAO,WAAW,MAAS;AACrE,wBAAY,WAAW,OAAO;AAC9B,mBAAO;AAAA,UACT,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,iBAAO;AAAA,YACL,IAAI;AAAA,YACJ,SAAS,kCAA6B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,YACtF,OAAO,KAAK,MAAM;AAAA,YAClB,WAAW,KAAK;AAAA,YAChB,YAAY,KAAK;AAAA,UACnB;AAAA,QACF;AACA,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,SAAS,4BAAuB,KAAK;AAAA,EAAe,SAAS;AAAA,UAC7D,OAAO,KAAK,MAAM;AAAA,UAClB,WAAW,KAAK;AAAA,UAChB,YAAY,KAAK;AAAA,QACnB;AAAA,MACF;AAGA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,OAAO,KAAK,MAAM;AAAA,QAClB,WAAW,KAAK;AAAA,QAChB,YAAY,KAAK;AAAA,MACnB;AAAA,IACF;AAEA,UAAM,IAAI,wBAAwB,KAAK,KAAK;AAC5C,UAAM,UAAU,YAAY,QAAQ,IAChC,qBAAgB,YAAY,KAAK,0BAA0B,YAAY,KAAK;AAAA,EAAO,eAAe,KAAK,KAAK,CAAC,KAC7G,KAAK,MAAM,SAAS,IAClB,eAAe,KAAK,KAAK,IACzB;AACN,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,SAAS;AAAA,MACT,OAAO,KAAK,MAAM;AAAA,MAClB,WAAW,EAAE;AAAA,MACb,YAAY,EAAE;AAAA,IAChB;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["mutateTasks", "mutatePlan", "board", "mutateTasks", "mutatePlan"]
|
|
7
7
|
}
|
package/dist/test.d.ts
CHANGED
package/dist/test.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,wBAAwB,CAAC;AAKpE,UAAU,SAAS;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAC1D,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/B;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAkHhD,CAAC"}
|