workbite 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -8
- package/lib/agent.js +47 -17
- package/lib/archive.js +110 -9
- package/lib/cli.js +79 -7
- package/lib/diagnostics.js +234 -0
- package/lib/install.js +44 -4
- package/lib/mcp.js +133 -22
- package/lib/runtime.js +188 -91
- package/lib/startup-lock.js +71 -0
- package/lib/transfer.js +113 -0
- package/package.json +1 -1
- package/release.json +8 -15
- package/skills/workbite-native/SKILL.md +18 -3
- package/skills/workbite-native/references/commands.md +26 -12
- package/skills/workbite-native/references/task-fields.md +208 -0
- package/skills/workbite-native/references/transfer.md +204 -0
package/lib/transfer.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Files only: the native client/owner owns all transfer HTTP, graph validation and commits.
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { constants } = require('node:fs');
|
|
6
|
+
const { randomBytes } = require('node:crypto');
|
|
7
|
+
const { fail, UUID } = require('./common');
|
|
8
|
+
|
|
9
|
+
const MAX_TRANSFER_BYTES = 1024 * 1024;
|
|
10
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
11
|
+
const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
function localPath(filename) {
|
|
13
|
+
if (typeof filename !== 'string' || !filename || /[\x00-\x1f]/.test(filename)
|
|
14
|
+
|| /^[a-z][a-z0-9+.-]*:\/\//i.test(filename) || /^(?:\\\\|\/\/)/.test(filename)) fail('Transfer files must be local filesystem paths');
|
|
15
|
+
return path.resolve(filename);
|
|
16
|
+
}
|
|
17
|
+
// Fail closed on links in the requested path. Do not create missing output directories.
|
|
18
|
+
async function realDirectory(directory, io = fs) {
|
|
19
|
+
const parent = path.dirname(directory);
|
|
20
|
+
if (parent !== directory) await realDirectory(parent, io);
|
|
21
|
+
const info = await io.lstat(directory);
|
|
22
|
+
if (!info.isDirectory() || info.isSymbolicLink()) fail('Transfer path contains a link or non-directory');
|
|
23
|
+
}
|
|
24
|
+
async function outputPath(filename, io = fs) {
|
|
25
|
+
const target = localPath(filename);
|
|
26
|
+
await realDirectory(path.dirname(target), io);
|
|
27
|
+
try { await io.lstat(target); } catch (error) { if (error.code === 'ENOENT') return target; throw error; }
|
|
28
|
+
fail('Transfer output already exists; choose a new file (overwrite is not supported)');
|
|
29
|
+
}
|
|
30
|
+
function archive(value, workspaceUid) {
|
|
31
|
+
if (!plain(value) || value.format !== 'workbite.task-transfer.v1'
|
|
32
|
+
|| typeof value.source_workspace_uid !== 'string' || !UUID.test(value.source_workspace_uid)
|
|
33
|
+
|| typeof value.source_digest !== 'string' || !DIGEST.test(value.source_digest)
|
|
34
|
+
|| !Array.isArray(value.tasks) || !value.tasks.every(plain)
|
|
35
|
+
|| !Array.isArray(value.objectives) || !value.objectives.every(plain)) fail('Invalid workbite.task-transfer.v1 archive');
|
|
36
|
+
if (workspaceUid !== undefined && value.source_workspace_uid !== workspaceUid) fail('Export archive does not match the bound source workspace');
|
|
37
|
+
if (Buffer.byteLength(JSON.stringify(value)) > MAX_TRANSFER_BYTES) fail(`Transfer archive exceeds ${MAX_TRANSFER_BYTES} bytes`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
async function readArchive(filename, io = fs) {
|
|
41
|
+
const target = localPath(filename);
|
|
42
|
+
await realDirectory(path.dirname(target), io);
|
|
43
|
+
const before = await io.lstat(target);
|
|
44
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_TRANSFER_BYTES) fail('Transfer input must be a regular file of at most 1048576 bytes');
|
|
45
|
+
const handle = await io.open(target, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
46
|
+
try {
|
|
47
|
+
const info = await handle.stat();
|
|
48
|
+
if (!info.isFile() || info.size > MAX_TRANSFER_BYTES || info.ino !== before.ino || info.dev !== before.dev) fail('Transfer input changed while opening');
|
|
49
|
+
// Read at most the bound plus one byte, even if another process grows the file.
|
|
50
|
+
const bytes = Buffer.alloc(MAX_TRANSFER_BYTES + 1);
|
|
51
|
+
let size = 0;
|
|
52
|
+
while (size < bytes.length) {
|
|
53
|
+
const read = await handle.read(bytes, size, bytes.length - size, null);
|
|
54
|
+
if (!read.bytesRead) break;
|
|
55
|
+
size += read.bytesRead;
|
|
56
|
+
}
|
|
57
|
+
if (size > MAX_TRANSFER_BYTES) fail(`Transfer archive exceeds ${MAX_TRANSFER_BYTES} bytes`);
|
|
58
|
+
let text, value;
|
|
59
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size)); value = JSON.parse(text); }
|
|
60
|
+
catch { fail('Transfer input must contain valid UTF-8 JSON'); }
|
|
61
|
+
archive(value);
|
|
62
|
+
return text;
|
|
63
|
+
} finally { await handle.close(); }
|
|
64
|
+
}
|
|
65
|
+
// Publish with an atomic hard link: rename would overwrite an existing destination on POSIX.
|
|
66
|
+
// The private temporary file is in the same directory; an unsupported link is a safe refusal.
|
|
67
|
+
async function writePrivate(filename, value, io = fs) {
|
|
68
|
+
const target = await outputPath(filename, io);
|
|
69
|
+
const bytes = Buffer.from(JSON.stringify(value));
|
|
70
|
+
if (bytes.length > MAX_TRANSFER_BYTES) fail(`Transfer output exceeds ${MAX_TRANSFER_BYTES} bytes`);
|
|
71
|
+
const temporary = path.join(path.dirname(target), `.workbite-transfer-${randomBytes(16).toString('hex')}.tmp`);
|
|
72
|
+
let handle;
|
|
73
|
+
try {
|
|
74
|
+
handle = await io.open(temporary, 'wx', 0o600);
|
|
75
|
+
await handle.writeFile(bytes);
|
|
76
|
+
await handle.sync();
|
|
77
|
+
await handle.close(); handle = null;
|
|
78
|
+
// Recheck the directory before publishing; link refuses even a raced-in destination.
|
|
79
|
+
await realDirectory(path.dirname(target), io);
|
|
80
|
+
await io.link(temporary, target);
|
|
81
|
+
} finally {
|
|
82
|
+
if (handle) await handle.close();
|
|
83
|
+
await io.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
84
|
+
}
|
|
85
|
+
return target;
|
|
86
|
+
}
|
|
87
|
+
function resultData(result, command, uid, intent) {
|
|
88
|
+
if (typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > MAX_TRANSFER_BYTES + 8192) fail('Invalid native transfer output');
|
|
89
|
+
let envelope;
|
|
90
|
+
try { envelope = JSON.parse(result.stdout); } catch { fail('Invalid native transfer envelope'); }
|
|
91
|
+
if (!plain(envelope) || envelope.contract !== 'workstack.cli.v1' || !plain(envelope.data)
|
|
92
|
+
|| envelope.error !== undefined || envelope.meta?.command !== `agent.${command}` || envelope.meta?.workspace_uid !== uid
|
|
93
|
+
|| (intent !== undefined && envelope.meta?.intent_id !== intent)) fail('Native transfer output does not match this workspace, command and intent');
|
|
94
|
+
return envelope.data;
|
|
95
|
+
}
|
|
96
|
+
function idMap(value) {
|
|
97
|
+
// The owner emits maps by source authority ID; values are generated target display IDs.
|
|
98
|
+
if (!plain(value) || Object.keys(value).some(key => !['tasks', 'task_uids', 'objectives', 'key_results'].includes(key))) fail('Invalid native transfer id_map');
|
|
99
|
+
for (const [kind, mapping] of Object.entries(value)) {
|
|
100
|
+
if (!plain(mapping)) fail('Invalid native transfer id_map');
|
|
101
|
+
for (const [source, target] of Object.entries(mapping)) {
|
|
102
|
+
if (!source || /[\x00-\x1f]/.test(source)) fail(`Invalid native transfer ${kind} mapping`);
|
|
103
|
+
if (kind === 'key_results') {
|
|
104
|
+
if (!plain(target) || Object.entries(target).some(([key, id]) => !key || /[\x00-\x1f]/.test(key)
|
|
105
|
+
|| typeof id !== 'string' || !id || /[\x00-\x1f]/.test(id))) fail('Invalid native transfer key_results mapping');
|
|
106
|
+
} else if (typeof target !== 'string' || !target || /[\x00-\x1f]/.test(target)
|
|
107
|
+
|| (kind === 'task_uids' && !UUID.test(target))) fail(`Invalid native transfer ${kind} mapping`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!plain(value.tasks)) fail('Native transfer id_map requires tasks');
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
module.exports = { MAX_TRANSFER_BYTES, DIGEST, readArchive, archive, outputPath, writePrivate, resultData, idMap };
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "workbite.release.v1",
|
|
3
3
|
"product": "WorkBite",
|
|
4
|
-
"version": "0.1.12",
|
|
5
|
-
"launcherVersion": "0.1.12",
|
|
6
4
|
"repository": "Shinick-Han/WorkBite-releases",
|
|
7
|
-
"tag": "v0.1.
|
|
5
|
+
"tag": "v0.1.14",
|
|
6
|
+
"version": "0.1.14",
|
|
7
|
+
"launcherVersion": "0.1.14",
|
|
8
8
|
"platforms": {
|
|
9
9
|
"windows-x64": {
|
|
10
|
-
"asset": "workbite-0.1.
|
|
11
|
-
"sha256": "
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.
|
|
15
|
-
},
|
|
16
|
-
"linux-x64": {
|
|
17
|
-
"asset": "workbite-0.1.12-linux-x64.tar.gz",
|
|
18
|
-
"sha256": "36630e7c11096df8dc5ef720ecfdada286037c696fe61d787903c8922c23c5ce",
|
|
19
|
-
"size": 81937308,
|
|
20
|
-
"bundleSha256": "d83eeea345571a02ef1f54c0b2eef1a50b7c711d176d52beaff13470dbfe31f8",
|
|
21
|
-
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.12/workbite-0.1.12-linux-x64.tar.gz"
|
|
10
|
+
"asset": "workbite-0.1.14-windows-x64.zip",
|
|
11
|
+
"sha256": "3cac949c2b9d5135dfa195425581b7f731d1e28eb6712069781f0f82f6adcdfd",
|
|
12
|
+
"bundleSha256": "459ae8b6667a13613824729e8b35c1df13c8d9fa77f757fbbcdc312f2c224742",
|
|
13
|
+
"size": 136146372,
|
|
14
|
+
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.14/workbite-0.1.14-windows-x64.zip"
|
|
22
15
|
}
|
|
23
16
|
}
|
|
24
17
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workbite-native
|
|
3
|
-
description: Read and record WorkBite Tasks through the installed `workbite agent` CLI or `workbite mcp` tools - check the bound workspace, list Tasks, read a Task's context, and, when the user asks for it (including a standing request made mid-work), create Tasks, update
|
|
3
|
+
description: Read and record WorkBite Tasks through the installed `workbite agent` CLI or `workbite mcp` tools - check the bound workspace, list Tasks, read a Task's context, and, when the user asks for it (including a standing request made mid-work), create Tasks, update advertised Task fields and append observable checkpoints with stable intent IDs. Use advertised export/import for explicitly requested Task graph transfers. Use for WorkBite / Work Stack task and context requests in a WorkBite install; never route them to a separately installed `workstack`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# WorkBite native Tasks
|
|
@@ -14,6 +14,11 @@ install, the Store files, or raw HTTP instead.
|
|
|
14
14
|
|
|
15
15
|
Exact argv, packets, envelopes and refusals: [references/commands.md](references/commands.md).
|
|
16
16
|
Read it before the first write in a session, and for MCP tool use.
|
|
17
|
+
For field types, nested-array replacement and create/update examples, read
|
|
18
|
+
[references/task-fields.md](references/task-fields.md). For an explicitly
|
|
19
|
+
requested cross-workspace Task transfer, read
|
|
20
|
+
[references/transfer.md](references/transfer.md). Transfer is capability-gated
|
|
21
|
+
unreleased source; this Skill does not upgrade an installed bundle.
|
|
17
22
|
|
|
18
23
|
## Bind: one workspace, path + UUID
|
|
19
24
|
|
|
@@ -46,8 +51,17 @@ the default binding.
|
|
|
46
51
|
intent exists, perform the writes it covers without asking again per write.
|
|
47
52
|
It does not extend to other Tasks, other workspaces or other kinds of
|
|
48
53
|
change.
|
|
54
|
+
- Probe `workbite agent capabilities` before writes. The installed command's
|
|
55
|
+
`packet_schema` is authoritative for every field and type, including
|
|
56
|
+
`changes.properties` for update. A command name alone does not prove field
|
|
57
|
+
support. If the schema is absent, use only that command's documented
|
|
58
|
+
original fields; report unavailable additions. Never use legacy `apply`,
|
|
59
|
+
another install or direct Store access to bypass an unavailable field.
|
|
49
60
|
- Create as many Tasks as the user's intent calls for, each with its own
|
|
50
|
-
intent ID.
|
|
61
|
+
intent ID. There is no five-Task reapproval cap; a transport size limit is
|
|
62
|
+
not a limit on the number of user-requested Tasks. `follows_task_id` is
|
|
63
|
+
optional provenance only. Existing targets must be named by the user or
|
|
64
|
+
unambiguously covered by the standing request; do not pick unrelated Tasks.
|
|
51
65
|
- Never mark a Task `done` or `dropped` on your own judgement; set status only
|
|
52
66
|
when the user asked for that status or the requested outcome is that status.
|
|
53
67
|
|
|
@@ -67,7 +81,8 @@ the default binding.
|
|
|
67
81
|
|
|
68
82
|
- One logical write = one stable intent ID (`[A-Za-z0-9._:-]{8,128}`), chosen
|
|
69
83
|
before sending and kept with its packet, e.g. `wb.create.20261013.login-bug`.
|
|
70
|
-
|
|
84
|
+
Receipt lookup precedes revision CAS: a replay of the same ID + identical
|
|
85
|
+
packet returns the original result even after later edits; a
|
|
71
86
|
different packet under a used ID is `idempotency_conflict`.
|
|
72
87
|
- `revision_conflict`: re-read context; re-issue against the current revision
|
|
73
88
|
with a new intent ID only if the change still fits what the user asked and
|
|
@@ -21,8 +21,8 @@ workbite agent [--workspace ABS --workspace-uid UUID] <command> ...
|
|
|
21
21
|
- `--workspace` (absolute path) and `--workspace-uid` (the workspace's UUID)
|
|
22
22
|
are given together or not at all.
|
|
23
23
|
- Without them, a read binds the workspace saved by the last successful
|
|
24
|
-
`workbite open`. Writes (`tasks create`, `tasks update`, `checkpoint
|
|
25
|
-
refuse without the explicit pair. Use the default only for discovery
|
|
24
|
+
`workbite open`. Writes (`tasks create`, `tasks update`, `checkpoint`,
|
|
25
|
+
import apply/rollback) refuse without the explicit pair. Use the default only for discovery
|
|
26
26
|
(`capabilities`, the first `status`, `tasks list`); once `status` has
|
|
27
27
|
named the workspace, pass the pair on `context` and `operation-status` too.
|
|
28
28
|
- The binding is checked before every call: the workspace descriptor must
|
|
@@ -59,12 +59,21 @@ workbite agent [--workspace ABS --workspace-uid UUID] <command> ...
|
|
|
59
59
|
entries. `--view` is `core-v1` (default), `planning-v1`, `planning-v2` or
|
|
60
60
|
`resume-v1`, when the native client lists it.
|
|
61
61
|
|
|
62
|
+
## Advertised fields and transfer commands
|
|
63
|
+
|
|
64
|
+
Probe `workbite agent capabilities` before using new fields or transfer
|
|
65
|
+
commands. Task fields require the exact command's `packet_schema`; MCP
|
|
66
|
+
`tools/list` must agree. See [task-fields.md](task-fields.md) for field types
|
|
67
|
+
and full packets, and [transfer.md](transfer.md) for unreleased,
|
|
68
|
+
capability-gated export/import preview/apply/status/rollback. A refusal never
|
|
69
|
+
authorizes legacy apply, raw HTTP or another install.
|
|
70
|
+
|
|
62
71
|
## Packets
|
|
63
72
|
|
|
64
73
|
Exactly one JSON object from `--input-file FILE` or `--stdin` (one is
|
|
65
74
|
required; they are exclusive), valid UTF-8 of at most 32768 bytes, with only
|
|
66
|
-
the keys
|
|
67
|
-
workspace. The launcher checks size and UTF-8 and hands the text to the native
|
|
75
|
+
the keys admitted by the advertised `packet_schema`. Write it without a BOM.
|
|
76
|
+
A packet never names a workspace. The launcher checks size and UTF-8 and hands the text to the native
|
|
68
77
|
client as stdin; the packet never travels in argv.
|
|
69
78
|
|
|
70
79
|
`tasks create`:
|
|
@@ -73,9 +82,10 @@ client as stdin; the packet never travels in argv.
|
|
|
73
82
|
{"title": "Fix login redirect loop", "detail": "Seen on /settings after token refresh.", "follows_task_id": "T-0004"}
|
|
74
83
|
```
|
|
75
84
|
|
|
76
|
-
`title` is required and
|
|
77
|
-
optional.
|
|
78
|
-
|
|
85
|
+
`title` is required and nonblank; `detail` and `follows_task_id` are
|
|
86
|
+
original optional fields. Additional fields require `packet_schema`; see
|
|
87
|
+
[task-fields.md](task-fields.md). `follows_task_id` is receipt provenance only
|
|
88
|
+
(no parent or dependency). One intent ID creates at most one Task.
|
|
79
89
|
|
|
80
90
|
`tasks update`:
|
|
81
91
|
|
|
@@ -84,8 +94,9 @@ dependency). One intent ID creates at most one Task.
|
|
|
84
94
|
```
|
|
85
95
|
|
|
86
96
|
`expected_revision` is the integer from a fresh `context`. `changes` is a
|
|
87
|
-
non-empty
|
|
88
|
-
`
|
|
97
|
+
non-empty object matching the advertised schema. The original subset is
|
|
98
|
+
`detail`/`status`; additions require explicit field/type advertisement.
|
|
99
|
+
See [task-fields.md](task-fields.md).
|
|
89
100
|
|
|
90
101
|
`checkpoint`:
|
|
91
102
|
|
|
@@ -113,7 +124,8 @@ apart later, for example `wb.create.20261013.login-bug`,
|
|
|
113
124
|
and `meta.replayed`. `tasks create` / `tasks update` add `data.receipt`
|
|
114
125
|
(`operation`, `result` `created` / `applied` / `no_change`, `task_id`,
|
|
115
126
|
`before_revision`, `after_revision`, `committed_at`) and `data.task`. A
|
|
116
|
-
replay returns the original snapshot
|
|
127
|
+
receipt lookup precedes revision CAS. A replay returns the original snapshot
|
|
128
|
+
even after later edits: read `context` for current state.
|
|
117
129
|
If `data.omitted` names `task` or `receipt`, they were dropped for size;
|
|
118
130
|
`commit_state` still settles the write.
|
|
119
131
|
- A write whose answer was lost or timed out exits 1 with a stderr message
|
|
@@ -181,8 +193,8 @@ or mistyped arguments are refused before anything runs.
|
|
|
181
193
|
| `workbite_status` | no | `workspace_uid` |
|
|
182
194
|
| `workbite_tasks_list` | no | `workspace_uid`, `status`, `limit`, `offset` |
|
|
183
195
|
| `workbite_context` | no | `workspace_uid`, **`task_id`**, `view`, `fingerprint`, `if_fingerprint` |
|
|
184
|
-
| `workbite_tasks_create` | yes | **`workspace_uid`**, **`intent_id`**, **`title`**, `detail`, `follows_task_id
|
|
185
|
-
| `workbite_tasks_update` | yes | **`workspace_uid`**, **`intent_id`**, **`task_id`**, **`expected_revision`**, **`changes`** (
|
|
196
|
+
| `workbite_tasks_create` | yes | **`workspace_uid`**, **`intent_id`**, **`title`**, `detail`, `follows_task_id`, other advertised create fields |
|
|
197
|
+
| `workbite_tasks_update` | yes | **`workspace_uid`**, **`intent_id`**, **`task_id`**, **`expected_revision`**, **`changes`** (advertised update fields) |
|
|
186
198
|
| `workbite_checkpoint` | yes | **`workspace_uid`**, **`intent_id`**, **`task_id`**, **`date`**, **`done`**, **`next`**, **`blockers`** |
|
|
187
199
|
| `workbite_operation_status` | no | `workspace_uid`, **`intent_id`** |
|
|
188
200
|
|
|
@@ -196,4 +208,6 @@ or mistyped arguments are refused before anything runs.
|
|
|
196
208
|
`commit_unknown` (with `meta.commit_state: "unknown"` and the `intent_id`
|
|
197
209
|
for `workbite_operation_status`). Calls run one at a time and are never
|
|
198
210
|
retried.
|
|
211
|
+
- Transfer tool names and exact preview/apply/status/rollback argument rules:
|
|
212
|
+
[transfer.md](transfer.md). They require actual native capability support.
|
|
199
213
|
- No tool runs shells, raw RPC, deletes or policy changes.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Advertised Task packet fields
|
|
2
|
+
|
|
3
|
+
Use a field only when the exact installed command's `packet_schema` admits
|
|
4
|
+
it: create's `properties`, or update's `properties.changes.properties`.
|
|
5
|
+
Probe the launcher and running owner selected by the Skill. A command key,
|
|
6
|
+
version, this reference or an MCP tool name alone does not prove support.
|
|
7
|
+
MCP `tools/list` must expose matching types too. With no schema, retain only
|
|
8
|
+
the documented original subset (create: title/detail/follows_task_id;
|
|
9
|
+
update: detail/status). Report missing requested fields; never silently omit
|
|
10
|
+
them or use legacy apply, another install or direct Store access.
|
|
11
|
+
|
|
12
|
+
These additive fields are unreleased source work, not a claim about any
|
|
13
|
+
installed bundle. The nested extension below is the NESTED FIELDS CONTRACT;
|
|
14
|
+
the native domain source implements these structures. Matching
|
|
15
|
+
Python/client/MCP completion and advertisements remain required before
|
|
16
|
+
claiming cross-implementation support. A working-source edit alone is not a
|
|
17
|
+
completed capability.
|
|
18
|
+
|
|
19
|
+
## Normal fields
|
|
20
|
+
|
|
21
|
+
| Field | JSON value and meaning |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `title` | Nonblank string; required on create. No string coercion. |
|
|
24
|
+
| `detail` | String, including empty string; no null or coercion. |
|
|
25
|
+
| `status` | `open`, `started`, `done`, `dropped`; only the status the user requested. |
|
|
26
|
+
| `priority` | `P0`, `P1`, `P2`, `P3`. |
|
|
27
|
+
| `due`, `scheduled` | Real ISO calendar date `YYYY-MM-DD`, or null to clear. |
|
|
28
|
+
| `estimate_minutes` | Integer 1–1440, or null to clear; booleans/fractions are invalid. |
|
|
29
|
+
| `tags` | Array of strings, normalized by the owner; `[]` clears. |
|
|
30
|
+
| `parent_id` | Existing Task ID, or null to clear; no self-parenting/cycles. |
|
|
31
|
+
| `dependencies` | Array of existing Task IDs, with graph validation; `[]` clears. |
|
|
32
|
+
| `objective_ids` | Array of existing Objective IDs; `[]` clears Task links. |
|
|
33
|
+
| `key_result_refs` | Array of exact `{objective_id, key_result_id}` string pairs, each an existing valid pair; `[]` clears. |
|
|
34
|
+
| `follows_task_id` | Optional existing Task ID on create only: receipt provenance, never a parent/dependency. |
|
|
35
|
+
|
|
36
|
+
Omission leaves a field unchanged on update and uses owner defaults on
|
|
37
|
+
create. Null clears only nullable fields; arrays use `[]`, never null.
|
|
38
|
+
Owner validation and normalization decide valid references, deduplication
|
|
39
|
+
and relationships. Task linking does not authorize Objective/Key Result CRUD.
|
|
40
|
+
|
|
41
|
+
`id`, `uid`, `created`, `created_at`, `updated_at`, `revision`, `status_fact_id`, actor
|
|
42
|
+
identity, audit and status facts are generated/protected. Do not inject them
|
|
43
|
+
or source history through ordinary packets. An explicitly requested create
|
|
44
|
+
status bootstraps fresh local status facts and audit; it cannot forge old
|
|
45
|
+
history. Normalized values appear in the committed `data.task` projection
|
|
46
|
+
when advertised, subject to the envelope's `data.omitted` size reporting.
|
|
47
|
+
|
|
48
|
+
## Complete create example
|
|
49
|
+
|
|
50
|
+
Only send this when all fields are advertised and the referenced records
|
|
51
|
+
exist in the verified workspace. Replace example values with the user's
|
|
52
|
+
requested values. Choose one stable intent ID before sending.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"title": "Verify login redirect",
|
|
57
|
+
"detail": "Cover refresh and return-to behavior.",
|
|
58
|
+
"status": "started",
|
|
59
|
+
"priority": "P1",
|
|
60
|
+
"due": "2026-10-20",
|
|
61
|
+
"scheduled": "2026-10-16",
|
|
62
|
+
"estimate_minutes": 45,
|
|
63
|
+
"tags": ["auth", "regression"],
|
|
64
|
+
"parent_id": "T-0001",
|
|
65
|
+
"dependencies": ["T-0002"],
|
|
66
|
+
"objective_ids": ["O-0001"],
|
|
67
|
+
"key_result_refs": [{"objective_id": "O-0001", "key_result_id": "KR-0001"}],
|
|
68
|
+
"follows_task_id": "T-0002"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
One intent creates at most one Task. A user-requested batch has one logical
|
|
73
|
+
write/intent per Task, with no five-Task reapproval cap. This is not an atomic
|
|
74
|
+
multi-Task import: use an advertised transfer operation for a whole graph.
|
|
75
|
+
|
|
76
|
+
## Complete update example
|
|
77
|
+
|
|
78
|
+
Read context for `T-0003` first and use its actual revision. This example
|
|
79
|
+
updates every normal field and demonstrates clearing nullable/array fields.
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"task_id": "T-0003",
|
|
84
|
+
"expected_revision": 4,
|
|
85
|
+
"changes": {
|
|
86
|
+
"title": "Verify login return-to behavior",
|
|
87
|
+
"detail": "Add a regression for the reported redirect.",
|
|
88
|
+
"status": "open",
|
|
89
|
+
"priority": "P2",
|
|
90
|
+
"due": null,
|
|
91
|
+
"scheduled": "2026-10-17",
|
|
92
|
+
"estimate_minutes": null,
|
|
93
|
+
"tags": [],
|
|
94
|
+
"parent_id": null,
|
|
95
|
+
"dependencies": [],
|
|
96
|
+
"objective_ids": [],
|
|
97
|
+
"key_result_refs": []
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`changes` is nonempty. Never put `follows_task_id` in update. The ordinary
|
|
103
|
+
packet limit stays 32768 UTF-8 bytes, without BOM or duplicate JSON keys.
|
|
104
|
+
The transfer archive limit does not enlarge ordinary Task packets.
|
|
105
|
+
|
|
106
|
+
## Notes and subtasks extension
|
|
107
|
+
|
|
108
|
+
These fields require explicit intent to replace the entire array, plus the
|
|
109
|
+
same Task revision CAS and durable receipt as other fields. Each array has
|
|
110
|
+
at most 128 items; `[]` clears it, null is invalid, and omission leaves it
|
|
111
|
+
unchanged on update (empty arrays by default on create). These are replacement
|
|
112
|
+
arrays, not append operations or partial child patches: include every note
|
|
113
|
+
or subtask the user wants retained. Read the current array before replacing
|
|
114
|
+
it; a bounded view that omitted it cannot establish what must be preserved.
|
|
115
|
+
|
|
116
|
+
| Field/item | Exact admitted structure |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `notes` | Array of objects with required `text` and optional `date`; no other keys. |
|
|
119
|
+
| note `text` | Nonblank string without NUL, trimmed by the owner. |
|
|
120
|
+
| note `date` | Real ISO calendar date `YYYY-MM-DD`; omitted means the owner's current civil day. Null is invalid. |
|
|
121
|
+
| `subtasks` | Array of objects with required `title` and optional `id`, `priority`, `status`; no other keys. |
|
|
122
|
+
| subtask `title` | Nonblank string without NUL, trimmed by the owner. |
|
|
123
|
+
| subtask `id` | Optional canonical `S-[1-9][0-9]*`, numeric suffix at most 9007199254740991; no leading zeros, unique within this parent's replacement array. |
|
|
124
|
+
| subtask `priority` | Optional `P0`/`P1`/`P2`/`P3`, default `P2`. |
|
|
125
|
+
| subtask `status` | Optional `open`/`started`/`done`/`dropped`, default `open`. |
|
|
126
|
+
|
|
127
|
+
Omitted Subtask IDs allocate deterministically, in input order, above the
|
|
128
|
+
maximum of IDs in the parent's existing array and explicitly supplied IDs.
|
|
129
|
+
Keep a retained subtask's ID explicitly; omitting it requests allocation,
|
|
130
|
+
not title matching. This is a parent-scoped display ID, not permission to
|
|
131
|
+
supply a Task ID/UUID, status fact or audit identity. Exhaustion refuses.
|
|
132
|
+
There is no claim of a separate durable Subtask high-water beyond that
|
|
133
|
+
existing/supplied-array rule.
|
|
134
|
+
|
|
135
|
+
A supplied note date is user note data, never evidence of when the local
|
|
136
|
+
system performed an action. Defaults resolve only after durable receipt
|
|
137
|
+
lookup: an identical same-intent replay on another day retains the original
|
|
138
|
+
note dates and allocated IDs. Normalized arrays appear in new committed
|
|
139
|
+
Task projections; older stored receipts can lack the new arrays.
|
|
140
|
+
|
|
141
|
+
When the user requested this content, a small create packet is:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"title": "Check redirect behavior",
|
|
146
|
+
"notes": [{"text": "User reported the refresh case."}],
|
|
147
|
+
"subtasks": [{"title": "Reproduce refresh"}]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
After reading `T-0003` and its actual revision, a whole-array replacement is:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"task_id": "T-0003",
|
|
156
|
+
"expected_revision": 4,
|
|
157
|
+
"changes": {
|
|
158
|
+
"notes": [{"text": "Confirmed the reported case.", "date": "2026-10-15"}],
|
|
159
|
+
"subtasks": [
|
|
160
|
+
{"id": "S-2", "title": "Reproduce refresh", "priority": "P1", "status": "done"},
|
|
161
|
+
{"title": "Verify return-to URL", "priority": "P2", "status": "open"}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Here an omitted ID is above both existing IDs and `S-2`; it is not necessarily
|
|
168
|
+
`S-3` if the current parent already has a higher ID. A subtask marked done
|
|
169
|
+
does not change the parent Task status. Code completion, dates, or child
|
|
170
|
+
statuses never authorize an automatic parent done transition.
|
|
171
|
+
|
|
172
|
+
## Observable field-change audit extension
|
|
173
|
+
|
|
174
|
+
The NESTED contract also requires bounded `field_changes` on the existing
|
|
175
|
+
`task.created`/`task.updated` activity event types for normal and nested
|
|
176
|
+
fields: each changed field has `before` and `after` observations. The owner
|
|
177
|
+
creates audit identity/time and status facts; callers never send those fields
|
|
178
|
+
in a Task packet. Large values may be represented by bounded digest/truncation
|
|
179
|
+
evidence instead of full content. Do not claim arbitrary historical values
|
|
180
|
+
can be recovered from such an event. A normalized no-change write still has
|
|
181
|
+
its own durable receipt and must not be presented as a new status transition.
|
|
182
|
+
This describes the extension contract, not completed cross-implementation
|
|
183
|
+
proof until the corresponding owner candidates pass review/integration.
|
|
184
|
+
|
|
185
|
+
## CAS, replay and uncertain receipts
|
|
186
|
+
|
|
187
|
+
An update's `expected_revision` is compare-and-swap (CAS). On a definite
|
|
188
|
+
`revision_conflict`, re-read; if the intended change remains valid without
|
|
189
|
+
overwriting someone else's change, send a new revision/packet with a new
|
|
190
|
+
intent ID. Existing user intent continues to apply.
|
|
191
|
+
|
|
192
|
+
Receipt lookup precedes CAS. Same intent + identical packet returns the
|
|
193
|
+
original committed snapshot even after later edits. Read context for current
|
|
194
|
+
state. Different content under that intent is `idempotency_conflict`.
|
|
195
|
+
|
|
196
|
+
A fresh intent is a distinct logical operation, not replay or content-based
|
|
197
|
+
deduplication. A fresh create can create another Task with identical text.
|
|
198
|
+
A fresh update rechecks CAS and recomputes omitted dates/IDs against current
|
|
199
|
+
state; it can be rejected or change values. Do not assume identical content
|
|
200
|
+
under different intents must yield no_change. Never use a fresh intent to
|
|
201
|
+
force an unresolved same-intent write through.
|
|
202
|
+
|
|
203
|
+
On response loss or `commit_unknown`, retain the same intent and packet
|
|
204
|
+
bytes, stop writing and read `operation-status`. `committed` settles it;
|
|
205
|
+
`unknown` never means not written. Only `reason: no_receipt` permits an
|
|
206
|
+
identical resend under the same intent. Never change its packet or switch to
|
|
207
|
+
a new intent to force an unknown write through. A checkpoint's unknown
|
|
208
|
+
outcome is reported without appending a replacement checkpoint.
|