trekoon 0.4.7 → 0.4.8

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.
@@ -106,6 +106,9 @@ continue broader epic execution only when it matches the user intent.
106
106
  current state.
107
107
  - Append progress, verification, and blocker notes with `--append`; do not
108
108
  rewrite descriptions unless fixing the plan itself.
109
+ - Compact specs are pipe-split. Before any `--task`/`--subtask`/`--dep`,
110
+ rephrase or escape bare `|` (especially `||` and trailing `|`) as `\|`.
111
+ See `reference/planning.md` "CAUTION — bare-pipe footguns".
109
112
  - Preview search/replace before `--apply`.
110
113
  - Never edit `.trekoon/trekoon.db` directly. Keep `.trekoon` gitignored.
111
114
  - Never run `trekoon wipe --yes --toon` unless the user explicitly asks.
@@ -115,3 +115,28 @@ trekoon --toon task done <task-id>
115
115
 
116
116
  `task done` auto-walks `todo` or `blocked` through `in_progress`. For subtasks,
117
117
  move through `in_progress` (claim or status) before `done`.
118
+
119
+ ## Compact Spec Hazards
120
+
121
+ Any batch creation command (`epic create`, `epic expand`, `task create-many`,
122
+ `subtask create-many`, `dep add-many`) splits `--task`/`--subtask`/`--dep`
123
+ values on raw `|`. Three recurring footguns — applies in plan and execute
124
+ modes (e.g. mid-execution `epic expand`):
125
+
126
+ 1. **Single mid-value `|`** with no explicit `|<status>` field: trailing
127
+ text silently lands in the status slot. Creation succeeds, fails on next
128
+ transition.
129
+ 2. **`||`** (JS logical-OR, shell OR): adds two extra fields per occurrence,
130
+ overshoots the field-count gate. Rephrase as "or" or escape as `\|\|`.
131
+ 3. **Trailing `|`** is not a terminator: creates an empty final field; on a
132
+ 4-field subtask shape that becomes an empty description and the parser
133
+ rejects with "is missing a description".
134
+
135
+ Escape literal `|` as `\|`. Pre-flight specs:
136
+
137
+ ```bash
138
+ grep -nE '(^|[^\\])\|\||\|$' specs.txt
139
+ ```
140
+
141
+ Full rules and the silent/loud failure matrix live in
142
+ `reference/planning.md` "CAUTION — bare-pipe footguns".
@@ -158,6 +158,11 @@ fires on a different shape — a single bare middle pipe like
158
158
  single-pipe / no-explicit-status case. Specs that already pass `|<status>`
159
159
  fail loudly even on a single unescaped `|`.
160
160
 
161
+ A bare `|` at the very end of a spec (trailing pipe) is **not** a terminator.
162
+ It produces an empty final field. On a `<...>|<title>|<description>` shape
163
+ that empty field becomes the description and the parser rejects the spec with
164
+ "is missing a description". Drop trailing `|`; never use it as a "done" marker.
165
+
161
166
  Spec shape (status optional, defaults to `todo`):
162
167
 
163
168
  - `--task <temp-key>|<title>|<description>` or `<temp-key>|<title>|<description>|<status>`
@@ -167,6 +172,12 @@ Spec shape (status optional, defaults to `todo`):
167
172
  Prefer the shorter form. Pass an explicit `|<status>` only when seeding a
168
173
  non-`todo` status.
169
174
 
175
+ The three bare-pipe footguns (`||`, single mid-value `|`, trailing `|`) and
176
+ the pre-flight `grep -nE '(^|[^\\])\|\||\|$'` recipe live in
177
+ `reference/harness-primitives.md` "Compact Spec Hazards". The paragraphs
178
+ above add the silent-vs-loud detail that the quick-ref points at. When in
179
+ doubt, build descriptions as plain prose without operator characters.
180
+
170
181
  One-shot rules:
171
182
 
172
183
  - Declare tasks/subtasks with plain temp keys, e.g. `task-api`, `sub-api-tests`.
@@ -100,7 +100,19 @@ trekoon --toon epic create \
100
100
 
101
101
  All temp keys (task and subtask) must be unique across the whole command — they share one flat namespace. Prefix subtask keys with the parent task key to stay unique.
102
102
 
103
- Escape any literal `|` inside field values as `\|`. A bare `|` on a spec without an explicit `|<status>` field silently pushes trailing text into the status slot (e.g. `Verify: bun test foo | tail` lets `tail` become the status, and creation still succeeds). Specs that already pass `|<status>` fail loudly on the same input. See the planning skill for full rules.
103
+ Escape any literal `|` inside field values as `\|`. Three recurring footguns:
104
+
105
+ - **Single mid-value `|`** on a spec without explicit `|<status>` silently pushes trailing text into the status slot (e.g. `Verify: bun test foo | tail` lets `tail` become the status, and creation still succeeds). Specs that already pass `|<status>` fail loudly on the same input.
106
+ - **`||`** (JS logical-OR `a || b`, shell OR `cmd || cmd`) adds two extra fields per occurrence; the field-count gate rejects with `Task specs must use ...` / `Subtask specs must use ...`. Rephrase `||` as "or" or escape as `\|\|`.
107
+ - **Trailing `|`** is not a terminator. It creates an empty final field; on a 4-field subtask shape that empty field becomes the description and the parser rejects with "is missing a description". Drop trailing pipes.
108
+
109
+ Pre-flight any batch before invoking `epic create`/`epic expand`/`task create-many`/`subtask create-many`:
110
+
111
+ ```bash
112
+ grep -nE '(^|[^\\])\|\||\|$' specs.txt
113
+ ```
114
+
115
+ See the planning skill for full rules.
104
116
 
105
117
  This is better than sequential creates because later records can reference
106
118
  earlier ones with `@temp-key`, and you get one atomic operation with mappings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "AI-first task tracking that lives in your repo. You describe what to build, your agent plans it as a dependency graph, then executes it task by task",
5
5
  "keywords": [
6
6
  "ai",
@@ -285,6 +285,58 @@ export function parseCompactFields(rawValue: string): ParsedCompactFields {
285
285
  };
286
286
  }
287
287
 
288
+ export function endsWithBareCompactPipe(rawSpec: string): boolean {
289
+ if (!rawSpec.endsWith("|")) {
290
+ return false;
291
+ }
292
+ let backslashes = 0;
293
+ for (let i = rawSpec.length - 2; i >= 0 && rawSpec[i] === "\\"; i--) {
294
+ backslashes++;
295
+ }
296
+ return backslashes % 2 === 0;
297
+ }
298
+
299
+ export function containsBareDoubleCompactPipe(rawSpec: string): boolean {
300
+ let escaping = false;
301
+ let prevBarePipe = false;
302
+ for (const character of rawSpec) {
303
+ if (escaping) {
304
+ escaping = false;
305
+ prevBarePipe = false;
306
+ continue;
307
+ }
308
+ if (character === "\\") {
309
+ escaping = true;
310
+ prevBarePipe = false;
311
+ continue;
312
+ }
313
+ if (character === "|") {
314
+ if (prevBarePipe) {
315
+ return true;
316
+ }
317
+ prevBarePipe = true;
318
+ continue;
319
+ }
320
+ prevBarePipe = false;
321
+ }
322
+ return false;
323
+ }
324
+
325
+ export function describeCompactPipeIssue(rawSpec: string): string {
326
+ const doublePipe = containsBareDoubleCompactPipe(rawSpec);
327
+ const trailingPipe = endsWithBareCompactPipe(rawSpec);
328
+ if (doublePipe && trailingPipe) {
329
+ return "Spec has bare `||` and ends with a trailing `|`. `||` (logical-OR or back-to-back pipes) adds two extra fields per occurrence; a trailing `|` creates an empty final field. Escape literal pipes as `\\|` or rephrase (e.g. `||` -> `or`), and drop the trailing `|`.";
330
+ }
331
+ if (doublePipe) {
332
+ return "Spec has bare `||` (two pipes back-to-back) — common with JS logical-OR (`a || b`) or shell OR (`cmd a || cmd b`). Every unescaped `|` adds a field, so `||` adds two extra fields. Escape literal pipes as `\\|` or rephrase the operator (e.g. `||` -> `or`).";
333
+ }
334
+ if (trailingPipe) {
335
+ return "Spec ends with a bare `|`. The trailing `|` is not a terminator — it creates an empty final field. Drop the trailing `|`.";
336
+ }
337
+ return "Bare `|` inside a field value is a field separator. Escape literal pipes as `\\|` or rephrase the value to avoid `|`.";
338
+ }
339
+
288
340
  export function parseCompactEntityRef(rawValue: string): CompactEntityRef {
289
341
  if (rawValue.startsWith(COMPACT_TEMP_KEY_PREFIX)) {
290
342
  const tempKey = rawValue.slice(COMPACT_TEMP_KEY_PREFIX.length);
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  SEARCH_REPLACE_FIELDS,
3
+ describeCompactPipeIssue,
4
+ endsWithBareCompactPipe,
3
5
  findUnknownOption,
4
6
  hasFlag,
5
7
  isValidCompactTempKey,
@@ -397,7 +399,11 @@ function failUnexpectedPositionals(command: string, unexpected: readonly string[
397
399
 
398
400
  function failEmptyCompactField(command: string, option: string, index: number, rawSpec: string, field: string): CliResult {
399
401
  const label = option === "task" ? "Task" : "Subtask";
400
- return failBatchSpec(command, `${label} spec ${index + 1} is missing a ${field}.`, {
402
+ let human = `${label} spec ${index + 1} is missing a ${field}.`;
403
+ if (field === "description" && endsWithBareCompactPipe(rawSpec)) {
404
+ human += " Spec ends with a bare `|` — the trailing `|` is not a terminator and creates an empty description field. Drop the trailing `|` and write an actual description.";
405
+ }
406
+ return failBatchSpec(command, human, {
401
407
  option,
402
408
  index,
403
409
  rawSpec,
@@ -465,7 +471,7 @@ function parseExpandTaskSpecs(rawSpecs: readonly string[]): { specs: CompactTask
465
471
  if (parsed.fields.length !== 3 && parsed.fields.length !== 4) {
466
472
  return {
467
473
  specs: [],
468
- error: failBatchSpec("epic.expand", `Task specs must use <temp-key>|<title>|<description> or <temp-key>|<title>|<description>|<status> in --task spec ${index + 1}.`, {
474
+ error: failBatchSpec("epic.expand", `Task specs must use <temp-key>|<title>|<description> or <temp-key>|<title>|<description>|<status> in --task spec ${index + 1}. ${describeCompactPipeIssue(rawSpec)}`, {
469
475
  option: "task",
470
476
  index,
471
477
  rawSpec,
@@ -561,7 +567,7 @@ function parseExpandSubtaskSpecs(rawSpecs: readonly string[]): { specs: CompactS
561
567
  if (parsed.fields.length !== 4 && parsed.fields.length !== 5) {
562
568
  return {
563
569
  specs: [],
564
- error: failBatchSpec("epic.expand", `Subtask specs must use <parent-ref>|<temp-key>|<title>|<description> or <parent-ref>|<temp-key>|<title>|<description>|<status> in --subtask spec ${index + 1}.`, {
570
+ error: failBatchSpec("epic.expand", `Subtask specs must use <parent-ref>|<temp-key>|<title>|<description> or <parent-ref>|<temp-key>|<title>|<description>|<status> in --subtask spec ${index + 1}. ${describeCompactPipeIssue(rawSpec)}`, {
565
571
  option: "subtask",
566
572
  index,
567
573
  rawSpec,
@@ -88,7 +88,30 @@ const QUICKSTART_TEXT = [
88
88
  " field/value details; batch prompts use a count-only confirmation (30s",
89
89
  " timeout, defaults to reject).",
90
90
  "",
91
- "8) Wipe (destructive recovery only)",
91
+ "8) Batch planning with compact specs",
92
+ " One-shot create: trekoon --toon epic create --title \"...\" --description \"...\" \\",
93
+ " --task \"<temp-key>|<title>|<description>\" \\",
94
+ " --subtask \"@<task-temp-key>|<temp-key>|<title>|<description>\" \\",
95
+ " --dep \"@<source>|@<depends-on>\"",
96
+ " Status is optional; append |<status> only when seeding non-todo.",
97
+ " Temp keys (--task and --subtask) share one flat namespace per command; prefix",
98
+ " subtask keys with the parent task key (e.g. sub-<task-key>-tests).",
99
+ "",
100
+ " Compact specs split on raw |. CAUTION — three bare-pipe footguns:",
101
+ " a) Single mid-value | with no explicit |<status>: trailing text silently",
102
+ " lands in the status slot. e.g. `Verify: bun test foo | tail` makes",
103
+ " \" tail\" the status; creation succeeds, fails on next transition.",
104
+ " b) || (JS logical-OR or shell OR) adds two extra fields per occurrence and",
105
+ " overshoots the field-count gate (Task/Subtask specs must use ...).",
106
+ " Rephrase as \"or\" or escape as \\|\\|.",
107
+ " c) Trailing | is NOT a terminator. It creates an empty final field; on a",
108
+ " 4-field subtask shape that becomes an empty description and the parser",
109
+ " rejects with \"is missing a description\". Drop trailing pipes.",
110
+ " Escape literal | as \\|. Pre-flight specs before submitting:",
111
+ " grep -nE '(^|[^\\\\])\\|\\||\\|$' specs.txt",
112
+ " Full rules: .agents/skills/trekoon/reference/planning.md (CAUTION block).",
113
+ "",
114
+ "9) Wipe (destructive recovery only)",
92
115
  " trekoon wipe --yes",
93
116
  " Removes the shared .trekoon directory for every worktree in the repo.",
94
117
  " Don't use this for routine cleanup, sync fixes, or gitignore issues.",
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  SEARCH_REPLACE_FIELDS,
3
+ describeCompactPipeIssue,
4
+ endsWithBareCompactPipe,
3
5
  findUnknownOption,
4
6
  hasFlag,
5
7
  isValidCompactTempKey,
@@ -249,7 +251,11 @@ function failConflictingTaskIds(optionTaskId: string, positionalTaskId: string):
249
251
  }
250
252
 
251
253
  function failEmptyCompactField(command: string, option: string, index: number, rawSpec: string, field: string): CliResult {
252
- return failBatchSpec(command, `${option === "subtask" ? "Subtask" : "Spec"} spec ${index + 1} is missing a ${field}.`, {
254
+ let human = `${option === "subtask" ? "Subtask" : "Spec"} spec ${index + 1} is missing a ${field}.`;
255
+ if (field === "description" && endsWithBareCompactPipe(rawSpec)) {
256
+ human += " Spec ends with a bare `|` — the trailing `|` is not a terminator and creates an empty description field. Drop the trailing `|` and write an actual description.";
257
+ }
258
+ return failBatchSpec(command, human, {
253
259
  option,
254
260
  index,
255
261
  rawSpec,
@@ -289,7 +295,7 @@ function parseSubtaskCreateManySpecs(parentTaskId: string, rawSpecs: readonly st
289
295
  if (parsed.fields.length !== 3 && parsed.fields.length !== 4) {
290
296
  return {
291
297
  specs: [],
292
- error: failBatchSpec("subtask.create-many", `Subtask specs must use <temp-key>|<title>|<description> or <temp-key>|<title>|<description>|<status> in --subtask spec ${index + 1}.`, {
298
+ error: failBatchSpec("subtask.create-many", `Subtask specs must use <temp-key>|<title>|<description> or <temp-key>|<title>|<description>|<status> in --subtask spec ${index + 1}. ${describeCompactPipeIssue(rawSpec)}`, {
293
299
  option: "subtask",
294
300
  index,
295
301
  rawSpec,
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  SEARCH_REPLACE_FIELDS,
3
+ describeCompactPipeIssue,
4
+ endsWithBareCompactPipe,
3
5
  findUnknownOption,
4
6
  hasFlag,
5
7
  isValidCompactTempKey,
@@ -338,7 +340,11 @@ function failUnexpectedPositionals(command: string, unexpected: readonly string[
338
340
  }
339
341
 
340
342
  function failEmptyCompactField(command: string, option: string, index: number, rawSpec: string, field: string): CliResult {
341
- return failBatchSpec(command, `${option === "task" ? "Task" : "Spec"} spec ${index + 1} is missing a ${field}.`, {
343
+ let human = `${option === "task" ? "Task" : "Spec"} spec ${index + 1} is missing a ${field}.`;
344
+ if (field === "description" && endsWithBareCompactPipe(rawSpec)) {
345
+ human += " Spec ends with a bare `|` — the trailing `|` is not a terminator and creates an empty description field. Drop the trailing `|` and write an actual description.";
346
+ }
347
+ return failBatchSpec(command, human, {
342
348
  option,
343
349
  index,
344
350
  rawSpec,
@@ -378,7 +384,7 @@ function parseTaskCreateManySpecs(rawSpecs: readonly string[]): { specs: Compact
378
384
  if (parsed.fields.length !== 3 && parsed.fields.length !== 4) {
379
385
  return {
380
386
  specs: [],
381
- error: failBatchSpec("task.create-many", `Task specs must use <temp-key>|<title>|<description> or <temp-key>|<title>|<description>|<status> in --task spec ${index + 1}.`, {
387
+ error: failBatchSpec("task.create-many", `Task specs must use <temp-key>|<title>|<description> or <temp-key>|<title>|<description>|<status> in --task spec ${index + 1}. ${describeCompactPipeIssue(rawSpec)}`, {
382
388
  option: "task",
383
389
  index,
384
390
  rawSpec,