@erclx/aitk 0.97.1 → 0.99.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +2 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +8 -8
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +3 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/tasks.md +30 -7
- package/docs/ai-workflow.md +3 -1
- package/package.json +1 -1
- package/src/commands/tasks.ts +19 -0
- package/src/tasks/validate.ts +169 -2
- package/standards/plan.md +12 -0
- package/standards/tasks.md +1 -1
|
@@ -124,6 +124,8 @@ What arrives there does not become a record by being read, so place it by what i
|
|
|
124
124
|
|
|
125
125
|
A plan written here is written against a tree several branches are already changing, so it names the file set of every track in flight as a constraint, one set per track, read from the Touches column of that track's row. State for each set which of the two acts it forbids, per Constraints in `.claude/standards/plan.md`, or `${CLAUDE_SKILL_DIR}/../../standards/plan.md` when the project does not have it. A bare path list leaves the worker guessing, which is how a plan ends up forbidding the repair of a citation the change broke.
|
|
126
126
|
|
|
127
|
+
Stamp the block with the commit this session read the tree at, which the same section fixes the form of. A plan written during a refill sits in the ready queue while the wave it names merges, so the constraint is true when written and false when a worker reads it. The stamp is what lets that worker test the difference, and the standard carries the test.
|
|
128
|
+
|
|
127
129
|
## Boundaries
|
|
128
130
|
|
|
129
131
|
- Run one orchestrator at a time. The board is gitignored, so a second session sees none of this one's writes: two task files land minutes apart under different labels for the same work, one session archives a task mid-sweep in the other, and each archives a plan the other had retargeted. An Owner column does not fix this, since neither session can read the other's rows.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Orchestrator parked row runbook
|
|
3
|
-
description: Re-testing every parked blocker against the current tree, the
|
|
3
|
+
description: Re-testing every parked blocker against the current tree, the two triggers that start the pass, and the two ways a re-test goes wrong
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Re-test every parked row as orchestrator. A blocker cell is a measurement taken the day the row was parked and nothing re-takes it, so a row can wait on a condition that stopped holding weeks earlier with no surface reporting the gap.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Two triggers start this pass. `orchestrator-sweep.md` ends by sending the rows `aitk tasks validate` listed as untested here, because a merge changes the tree under every parked row at once rather than under the rows naming it. The other is an idle session: nothing merged, workers are building, no pull request is waiting on a first pass, and the board is not moving.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The two differ in scope rather than in procedure. A merge sends the untested rows, since the validator already re-took the two kinds it can settle. An idle session walks every parked row, because no event narrowed which of them to look at.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Neither trigger is a scheduler. `orchestrator-poll.md` owns the one recurring trigger this skill has, and a second loop firing into a static board is the always-on failure that file already warns about.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
The sweep's own question stays distinct from this one. It asks which parked row to promote next, taking the blocker cell as read, and this pass asks whether that cell is still true. A session collapsing them runs whichever it remembers and re-tests nothing.
|
|
15
15
|
|
|
16
16
|
## Scope
|
|
17
17
|
|
|
@@ -21,10 +21,10 @@ Take the rows in board order and finish one before opening the next. Clearing a
|
|
|
21
21
|
|
|
22
22
|
## Re-testing a row
|
|
23
23
|
|
|
24
|
-
The blocker cell states what the row waits on, and each kind is tested differently.
|
|
24
|
+
The blocker cell states what the row waits on, and each kind is tested differently. `aitk tasks validate` already re-takes the first two and reports the rest as untested, so run it first and re-take by hand only what it names.
|
|
25
25
|
|
|
26
|
-
- Collision with a track in flight:
|
|
27
|
-
- A dependency on another task:
|
|
26
|
+
- Collision with a track in flight: the validator tests the file the cell cites against the Touches column of every `## Run now` row. A track that merged since the row was parked is no longer in flight, whatever the sets still share. A cell naming the file in prose rather than in backticks cites nothing, so write the collision the way the board format spells it and the check picks the row up on the next run.
|
|
27
|
+
- A dependency on another task: the validator opens the task a link in the cell names. One whose outcomes are all `[x]`, or one already archived, holds nothing. A cell naming the task in prose resolves to no file, so open it by hand and rewrite the cell as a link.
|
|
28
28
|
- A condition about the tree, such as a count of some shape or the presence of a construct: measure it again, per Two ways a re-test goes wrong below.
|
|
29
29
|
- Waiting on a plan: nothing external holds the row, so the pass writes the plan rather than testing anything. See The plan half below.
|
|
30
30
|
- Waiting on an operator action, such as a run that happens from a shell: record it as untestable this pass and name what the operator has to do. A session cannot clear it, and re-measuring it every pass is waste.
|
|
@@ -10,7 +10,9 @@ Sweep the board as orchestrator after merging. Run this once per batch of merges
|
|
|
10
10
|
3. Re-verify every plan already written, not only the ones this sweep writes. A queued plan goes stale from whatever merged while it waited, and the loop's verify step fires at handoff rather than after a merge, so nothing else catches it. Grep each construct the plan names and count the sites against its claim, then open each file rather than trusting its account.
|
|
11
11
|
4. Re-check any precondition a plan states about live state outside the repository. A remote branch, an open issue, or an installed version was true when the plan was written and is not a fact about the tree.
|
|
12
12
|
|
|
13
|
-
Run `aitk tasks validate` once the board is rewritten and before reporting it. It resolves every plan pointer, maps rows against task files both ways,
|
|
13
|
+
Run `aitk tasks validate` once the board is rewritten and before reporting it. It resolves every plan pointer, maps rows against task files both ways, tests the `## Run now` file sets for overlap, and re-takes the two blocker kinds a command can settle. It reports and never writes, so fix each row it names and run it again. A finding it reports is a board defect rather than a task finding, so it goes nowhere but the board.
|
|
14
|
+
|
|
15
|
+
Then run `orchestrator-parked.md` over the rows the validator listed as untested. Those carry the three blocker kinds no command settles, and the merge this sweep followed changed the tree under every one of them at once while the refill above re-read none. Take the untested rows alone rather than the whole board, since the validator already answered the rest.
|
|
14
16
|
|
|
15
17
|
Place every finding the merged work produced before promoting anything. A finding that changes a rule goes to the standard or the rule that states it, one that changes another task goes to that task's Findings, and one that overturns a groundwork lean gets marked answered in that folder. A pull request body counts as nowhere, since the thread stops being read the moment it merges.
|
|
16
18
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -29,7 +29,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
29
29
|
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
|
|
30
30
|
| `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
|
|
31
31
|
| `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
|
|
32
|
-
| `aitk tasks validate` | Report board rows whose plan, task file, group,
|
|
32
|
+
| `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`) |
|
|
33
33
|
| `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
|
|
34
34
|
| `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
|
|
35
35
|
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
package/docs/agents/tasks.md
CHANGED
|
@@ -90,17 +90,40 @@ aitk tasks validate
|
|
|
90
90
|
aitk tasks validate --json
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Five checks run. Plan and Collisions reach one half each of the `## Run now` test the board standard states. Mapping and Grouping test the folder contract and hold for every group. Blockers reaches the rows outside `## Run now`:
|
|
94
94
|
|
|
95
|
-
| Check | What it reports
|
|
96
|
-
| ---------- |
|
|
97
|
-
| Plan | A `## Run now` row whose Plan column carries no link, or one resolving to no file
|
|
98
|
-
| Mapping | A row naming no task file, and a task file no row names
|
|
99
|
-
| Grouping | A task carrying a row in more than one readiness group
|
|
100
|
-
| Collisions | Two `## Run now` rows whose Touches columns name a path in common
|
|
95
|
+
| Check | What it reports |
|
|
96
|
+
| ---------- | ------------------------------------------------------------------------------------ |
|
|
97
|
+
| Plan | A `## Run now` row whose Plan column carries no link, or one resolving to no file |
|
|
98
|
+
| Mapping | A row naming no task file, and a task file no row names |
|
|
99
|
+
| Grouping | A task carrying a row in more than one readiness group |
|
|
100
|
+
| Collisions | Two `## Run now` rows whose Touches columns name a path in common |
|
|
101
|
+
| Blockers | A parked row whose blocker has stopped holding, or whose cited task resolves nowhere |
|
|
101
102
|
|
|
102
103
|
The collision check is the one a person cannot run by eye. Paths come from the backticked spans in the Touches column, a span naming no file is dropped, and a directory collides with any file beneath it. A `## Run now` row whose column parses to nothing is reported rather than skipped, since a row stating no file set makes a claim nothing can check.
|
|
103
104
|
|
|
105
|
+
The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or closing every outcome, and a collision is settled by nothing under `## Run now` still holding the file the cell cites.
|
|
106
|
+
|
|
107
|
+
Both halves gate on a citation inside the `Waiting on` cell, never on the columns beside it. The board format gives a collision cell the file held by the running task, so a row whose cell names no file was parked by something else, and testing its Touches column instead reports a cleared collision on a row no collision ever parked while counting that row as re-tested. A cited task is a bare sibling link, the way the Task column spells one, so a pointer into another folder names a plan rather than a task and settles nothing. A cited task carrying no outcome box settles nothing either, since a file the check could not parse is not evidence of a finished one.
|
|
108
|
+
|
|
109
|
+
A citation resolving in neither the board nor the archive is `blocker-unresolved` rather than a settled row. Reading an absent file as archived states a specific fact about a file nobody ever wrote, which is what a renamed task or a typo produces, and only a task that genuinely closed releases the row waiting on it.
|
|
110
|
+
|
|
111
|
+
The other three kinds rest on a person's judgment, so a row neither half reached lands in a second array rather than in the findings:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"untested": [
|
|
116
|
+
{
|
|
117
|
+
"group": "Needs a plan",
|
|
118
|
+
"subject": "v50.6-a-standard-no-skill-reads",
|
|
119
|
+
"message": "..."
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
An untested row is not a finding and moves no exit code. Reading a clean findings list as a clean board is the failure the array exists to prevent, and `orchestrator-parked.md` is the pass that takes those rows by hand.
|
|
126
|
+
|
|
104
127
|
Exit codes: `0` every check passed, `1` refused, `2` at least one finding. The `reason` field carries which gate refused: `no-board`, `no-ordering`, or `no-groups`. A board grouping under headings of its own trips `no-groups` rather than being read against columns it never declared.
|
|
105
128
|
|
|
106
129
|
Columns are read from each table's own header rather than by position, so a project whose board differs from this one is reported for what it lacks. The `index` and `priority` siblings are skipped, along with every pre-compaction handoff, which takes one file per session under a `session-` prefix. None of them is a task, and a handoff counted as one would be reported as a task carrying no row on every session that wrote one.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -81,6 +81,8 @@ Run one orchestrator at a time. The board is gitignored, so a second session rea
|
|
|
81
81
|
|
|
82
82
|
Before a handoff, the orchestrator checks the plan against the tree rather than reading it: grep each construct it names and count the sites, confirm every phase label it cites is still open, and open each file it describes. A plan goes stale from whatever merged after it was written, and reading cannot catch that.
|
|
83
83
|
|
|
84
|
+
A constraint naming a track in flight carries the same problem past the handoff, so the block opens with the commit it was measured against. A worker re-tests before honoring one, fetching and then logging that commit against `origin/main` over the paths the constraint names, and any merge there means the track landed and the constraint is dead. An unstamped block reads as unverified rather than as live, which covers every plan written before the rule.
|
|
85
|
+
|
|
84
86
|
`.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. A session inside a worktree reads them directly, since the file-editing tools refuse a main-root path but `Read` resolves normally. It writes a whole file through the shell and makes a change inside an existing file through an `aitk` verb, which resolves the main root in-process. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
|
|
85
87
|
|
|
86
88
|
The plan's shape is fixed by `.claude/standards/plan.md`: the section list, the filename, the lifecycle, and the contract its questions keep. Every question carries a `- Suggested:` line and an empty `- Answer:` slot, and a blank answer accepts the suggestion at execution time. That default is what makes a plan decision-ready in one pass, and it is the opposite of the contract an intake folder keeps, where an empty slot means nobody reached the item.
|
|
@@ -103,7 +105,7 @@ The sweep reads the whole board rather than the tasks the session touched. It is
|
|
|
103
105
|
|
|
104
106
|
`.claude/memory/` carries the same arrangement, its own hook regenerating `index.md` from each entry's `title`, `description`, and `category`. A hand-maintained `priority.md` sits beside it carrying execution order and what each task is waiting on, which the alphabetical index cannot express.
|
|
105
107
|
|
|
106
|
-
`aitk tasks validate` checks what those rows claim against what the tree holds: every plan pointer resolves, every row and task file map one to one, no task sits in two groups, and no two rows marked ready touch the same file. That
|
|
108
|
+
`aitk tasks validate` checks what those rows claim against what the tree holds: every plan pointer resolves, every row and task file map one to one, no task sits in two groups, and no two rows marked ready touch the same file. That fourth check is the half a reader cannot run by eye, and it is what keeps two workers from being handed colliding work. A fifth re-takes what a parked row waits on, reporting one whose cited task has closed and one whose cited file nothing running still holds. Both halves read a citation out of the blocker cell, so a row citing neither is reported as untested rather than counted clean, which is where the three kinds resting on a person's judgment land. It reports and never writes, because a row is the orchestrator's claim and a validator repairing one would assert the claim it exists to test. Nothing fires it automatically, since the board is gitignored per-machine scratch with no shared moment to hang a hook on, so the orchestrator's sweep calls it at the point the readiness claim is made and follows it with the parked re-test.
|
|
107
109
|
|
|
108
110
|
`aitk:claude-tasks` owns the two operations that bracket a task's life. It creates the file, holding the filename convention and the frontmatter contract so a malformed write cannot break the index for every sibling, and it moves a shipped task to `.claude/task-archive/`. Creation is where the origin invariant is enforced: every task names a plan, a groundwork folder, an intake folder, or an issue, since a task with no origin is either lost context or work nobody decided to do.
|
|
109
111
|
|
package/package.json
CHANGED
package/src/commands/tasks.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@/tasks/record'
|
|
12
12
|
import {
|
|
13
13
|
type Finding,
|
|
14
|
+
type Untested,
|
|
14
15
|
type ValidateOutcome,
|
|
15
16
|
validateBoard,
|
|
16
17
|
} from '@/tasks/validate'
|
|
@@ -458,6 +459,19 @@ function reportValidation(
|
|
|
458
459
|
} else {
|
|
459
460
|
for (const finding of outcome.findings) logWarn(describe(finding))
|
|
460
461
|
}
|
|
462
|
+
|
|
463
|
+
// The untested rows carry the warn glyph rather than the pass glyph. They
|
|
464
|
+
// move no exit code, and a green tick on a row nothing re-took is the
|
|
465
|
+
// misread this section exists to prevent.
|
|
466
|
+
logStep('Parked rows')
|
|
467
|
+
if (outcome.untested.length === 0) {
|
|
468
|
+
logInfo('every parked row carried a citation or a file set to re-test')
|
|
469
|
+
} else {
|
|
470
|
+
logWarn(
|
|
471
|
+
`${outcome.untested.length} row(s) carry a blocker no check can settle`,
|
|
472
|
+
)
|
|
473
|
+
for (const row of outcome.untested) logWarn(describeUntested(row))
|
|
474
|
+
}
|
|
461
475
|
outro()
|
|
462
476
|
}
|
|
463
477
|
|
|
@@ -469,6 +483,7 @@ function reportValidation(
|
|
|
469
483
|
rows: outcome.rows,
|
|
470
484
|
tasks: outcome.tasks,
|
|
471
485
|
findings: outcome.findings,
|
|
486
|
+
untested: outcome.untested,
|
|
472
487
|
})}\n`,
|
|
473
488
|
)
|
|
474
489
|
}
|
|
@@ -481,6 +496,10 @@ function describe(finding: Finding): string {
|
|
|
481
496
|
return `${scope}${finding.subject} ${finding.message}`
|
|
482
497
|
}
|
|
483
498
|
|
|
499
|
+
function describeUntested(row: Untested): string {
|
|
500
|
+
return `${row.group}: ${row.subject} ${row.message}`
|
|
501
|
+
}
|
|
502
|
+
|
|
484
503
|
async function runArchive(
|
|
485
504
|
task: string | undefined,
|
|
486
505
|
opts: ArchiveCommandOptions,
|
package/src/tasks/validate.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises'
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
archiveDir,
|
|
6
|
+
isReservedStem,
|
|
7
|
+
readOutcomes,
|
|
8
|
+
tasksDir,
|
|
9
|
+
} from '@/tasks/archive'
|
|
5
10
|
|
|
6
11
|
const ORDERING_FILE = 'priority.md'
|
|
7
12
|
|
|
@@ -31,6 +36,8 @@ export const FINDING_KINDS = [
|
|
|
31
36
|
'row-duplicated',
|
|
32
37
|
'touches-unstated',
|
|
33
38
|
'touches-collided',
|
|
39
|
+
'blocker-settled',
|
|
40
|
+
'blocker-unresolved',
|
|
34
41
|
] as const
|
|
35
42
|
|
|
36
43
|
export type FindingKind = (typeof FINDING_KINDS)[number]
|
|
@@ -42,6 +49,17 @@ export interface Finding {
|
|
|
42
49
|
readonly message: string
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A parked row neither half of the blocker check reached. Three of the five
|
|
54
|
+
* blocker kinds put no fact on disk, so a run reporting findings alone would
|
|
55
|
+
* read as a board with nothing stale on it.
|
|
56
|
+
*/
|
|
57
|
+
export interface Untested {
|
|
58
|
+
readonly group: BoardGroup
|
|
59
|
+
readonly subject: string
|
|
60
|
+
readonly message: string
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
export interface BoardRow {
|
|
46
64
|
readonly group: BoardGroup
|
|
47
65
|
readonly label: string
|
|
@@ -49,6 +67,8 @@ export interface BoardRow {
|
|
|
49
67
|
readonly plan: string | undefined
|
|
50
68
|
/** Absent when the group fixes no `Touches` column, empty when it read none. */
|
|
51
69
|
readonly touches: readonly string[] | undefined
|
|
70
|
+
/** Absent when the group fixes no `Waiting on` column, which is `## Run now`. */
|
|
71
|
+
readonly waiting: string | undefined
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
export interface ValidateReport {
|
|
@@ -56,6 +76,7 @@ export interface ValidateReport {
|
|
|
56
76
|
readonly rows: number
|
|
57
77
|
readonly tasks: number
|
|
58
78
|
readonly findings: readonly Finding[]
|
|
79
|
+
readonly untested: readonly Untested[]
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
export interface ValidateRefused {
|
|
@@ -185,6 +206,7 @@ export function readBoard(text: string): {
|
|
|
185
206
|
const taskAt = columnIndex(header, 'task')
|
|
186
207
|
const planAt = columnIndex(header, 'plan')
|
|
187
208
|
const touchesAt = columnIndex(header, 'touches')
|
|
209
|
+
const waitingAt = columnIndex(header, 'waiting on')
|
|
188
210
|
|
|
189
211
|
const task = taskAt >= 0 ? (cells[taskAt] ?? '') : ''
|
|
190
212
|
const target = linkTarget(task)
|
|
@@ -196,6 +218,7 @@ export function readBoard(text: string): {
|
|
|
196
218
|
stem: target ? stemOf(target) : undefined,
|
|
197
219
|
plan,
|
|
198
220
|
touches: touchesAt >= 0 ? readPaths(cells[touchesAt] ?? '') : undefined,
|
|
221
|
+
waiting: waitingAt >= 0 ? (cells[waitingAt] ?? '') : undefined,
|
|
199
222
|
})
|
|
200
223
|
}
|
|
201
224
|
|
|
@@ -359,6 +382,142 @@ function checkCollisions(rows: readonly BoardRow[]): Finding[] {
|
|
|
359
382
|
return findings
|
|
360
383
|
}
|
|
361
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Reads the task a blocker cell cites. A task pointer is a bare sibling
|
|
387
|
+
* filename, the way every `Task` column spells one, so a target carrying a
|
|
388
|
+
* directory names something else and yields nothing. A row waiting on a plan
|
|
389
|
+
* links that plan, and reading its stem as a task would report the row settled
|
|
390
|
+
* against a folder the plan does not sit in.
|
|
391
|
+
*/
|
|
392
|
+
function citedStem(cell: string): string | undefined {
|
|
393
|
+
const target = linkTarget(cell)?.split('#')[0]
|
|
394
|
+
if (!target || target.includes('/')) return undefined
|
|
395
|
+
return stemOf(target)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Reports what a cited task does to the row waiting on it. A live file whose
|
|
400
|
+
* outcomes are all closed settles the row, and so does one sitting in the
|
|
401
|
+
* archive. A file carrying no outcome box settles nothing, since a file the
|
|
402
|
+
* check could not parse is not evidence of a finished one.
|
|
403
|
+
*
|
|
404
|
+
* A citation resolving in neither folder is a broken pointer rather than a
|
|
405
|
+
* closed task, and the two take different findings. Reading an absent file as
|
|
406
|
+
* archived states a specific fact about a file nobody ever wrote, which is what
|
|
407
|
+
* a renamed task or a typo produces.
|
|
408
|
+
*
|
|
409
|
+
* The outcome list comes off `readOutcomes` rather than a pattern of its own,
|
|
410
|
+
* so this check cannot disagree with the archive and outcome verbs about which
|
|
411
|
+
* checkboxes are outcomes and which sit inside a block a task displays.
|
|
412
|
+
*/
|
|
413
|
+
async function checkCitedTask(
|
|
414
|
+
group: BoardGroup,
|
|
415
|
+
subject: string,
|
|
416
|
+
cited: string,
|
|
417
|
+
root: string,
|
|
418
|
+
): Promise<Finding[]> {
|
|
419
|
+
const live = join(tasksDir(root), `${cited}.md`)
|
|
420
|
+
|
|
421
|
+
if (!existsSync(live)) {
|
|
422
|
+
if (existsSync(join(archiveDir(root), `${cited}.md`))) {
|
|
423
|
+
return [
|
|
424
|
+
{
|
|
425
|
+
kind: 'blocker-settled',
|
|
426
|
+
group,
|
|
427
|
+
subject,
|
|
428
|
+
message: `waits on ${cited}, which is archived.`,
|
|
429
|
+
},
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return [
|
|
434
|
+
{
|
|
435
|
+
kind: 'blocker-unresolved',
|
|
436
|
+
group,
|
|
437
|
+
subject,
|
|
438
|
+
message: `waits on ${cited}, which is neither on the board nor archived.`,
|
|
439
|
+
},
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const { open, closed } = readOutcomes(await readFile(live, 'utf8'))
|
|
444
|
+
if (open.length > 0 || closed.length === 0) return []
|
|
445
|
+
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
kind: 'blocker-settled',
|
|
449
|
+
group,
|
|
450
|
+
subject,
|
|
451
|
+
message: `waits on ${cited}, which carries no open outcome.`,
|
|
452
|
+
},
|
|
453
|
+
]
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Re-takes the two blocker kinds a command can settle, over every row outside
|
|
458
|
+
* `## Run now`. A cited task is settled by being archived or by closing every
|
|
459
|
+
* outcome, and a cited path is settled by no `## Run now` row still holding it.
|
|
460
|
+
*
|
|
461
|
+
* Both halves gate on a citation the cell carries, never on a column beside it.
|
|
462
|
+
* The board standard gives a collision cell the file held by the running task,
|
|
463
|
+
* so a row whose cell names no file was parked by something else and its
|
|
464
|
+
* `Touches` column says nothing about what holds it. Reading that column
|
|
465
|
+
* instead reports a cleared collision on a row no collision ever parked, and
|
|
466
|
+
* counts the row as re-tested, which is the more expensive half of that error.
|
|
467
|
+
*
|
|
468
|
+
* The cell is read for citations rather than parsed into fields, since the
|
|
469
|
+
* standard fixes three forms for it and leaves it prose. A row neither half
|
|
470
|
+
* reached is returned as untested, because the other three kinds are a person's
|
|
471
|
+
* to judge and silence about them reads as a board with nothing stale on it.
|
|
472
|
+
*/
|
|
473
|
+
async function checkParked(
|
|
474
|
+
rows: readonly BoardRow[],
|
|
475
|
+
root: string,
|
|
476
|
+
): Promise<{ findings: Finding[]; untested: Untested[] }> {
|
|
477
|
+
const findings: Finding[] = []
|
|
478
|
+
const untested: Untested[] = []
|
|
479
|
+
const running = rows.filter((row) => row.group === 'Run now')
|
|
480
|
+
|
|
481
|
+
for (const row of rows) {
|
|
482
|
+
if (row.group === 'Run now') continue
|
|
483
|
+
|
|
484
|
+
const subject = row.stem ?? row.label
|
|
485
|
+
const cell = row.waiting ?? ''
|
|
486
|
+
const cited = citedStem(cell)
|
|
487
|
+
const contested = readPaths(cell)
|
|
488
|
+
|
|
489
|
+
if (cited) {
|
|
490
|
+
findings.push(...(await checkCitedTask(row.group, subject, cited, root)))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const held = contested.filter((path) =>
|
|
494
|
+
running.some((run) =>
|
|
495
|
+
(run.touches ?? []).some((other) => sharesPath(path, other)),
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if (contested.length > 0 && held.length === 0) {
|
|
500
|
+
findings.push({
|
|
501
|
+
kind: 'blocker-settled',
|
|
502
|
+
group: row.group,
|
|
503
|
+
subject,
|
|
504
|
+
message: `waits on ${contested.join(', ')}, which nothing under Run now holds.`,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!cited && contested.length === 0) {
|
|
509
|
+
untested.push({
|
|
510
|
+
group: row.group,
|
|
511
|
+
subject,
|
|
512
|
+
message:
|
|
513
|
+
'cites no task and no file, so neither half of its blocker is mechanical.',
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { findings, untested }
|
|
519
|
+
}
|
|
520
|
+
|
|
362
521
|
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
363
522
|
return { ok: false, reason, message }
|
|
364
523
|
}
|
|
@@ -389,12 +548,20 @@ export async function validateBoard(root: string): Promise<ValidateOutcome> {
|
|
|
389
548
|
}
|
|
390
549
|
|
|
391
550
|
const stems = await listTaskStems(dir)
|
|
551
|
+
const parked = await checkParked(rows, root)
|
|
392
552
|
|
|
393
553
|
const findings = [
|
|
394
554
|
...checkMapping(rows, stems, dir),
|
|
395
555
|
...checkPlans(rows, dir, root),
|
|
396
556
|
...checkCollisions(rows),
|
|
557
|
+
...parked.findings,
|
|
397
558
|
]
|
|
398
559
|
|
|
399
|
-
return {
|
|
560
|
+
return {
|
|
561
|
+
ok: true,
|
|
562
|
+
rows: rows.length,
|
|
563
|
+
tasks: stems.length,
|
|
564
|
+
findings,
|
|
565
|
+
untested: parked.untested,
|
|
566
|
+
}
|
|
400
567
|
}
|
package/standards/plan.md
CHANGED
|
@@ -71,6 +71,16 @@ A constraint naming a surface to leave alone forbids two different acts, and it
|
|
|
71
71
|
- Never forbid retargeting a pointer the change breaks. A rename, a split, or a deletion that leaves a citation behind ships a dangling reference, so repairing it is required work rather than scope creep.
|
|
72
72
|
- Decide both acts for every surface the constraint names. Carving the distinction out for one file and leaving its siblings under the bare wording ships one correct call beside one broken reference.
|
|
73
73
|
|
|
74
|
+
A constraint measured against work in flight expires when that work merges, and a plan is read some time after it is written. Stamp the block with what it was measured against so a reader can test whether it still holds.
|
|
75
|
+
|
|
76
|
+
- Give the stamp its own leading bullet, written as Measured against `<commit>` on <YYYY-MM-DD>. One stamp covers the whole block however many tracks the constraints below it name, since a plan is written against the tree once.
|
|
77
|
+
- Stamp the commit and not the track names. The constraint already names each track by its work and its file set, so a second list of names is a second place to keep in step. A date alone does not separate two plans written the same afternoon on either side of three merges, which is why the commit is the part a reader tests against.
|
|
78
|
+
- Re-test a stamped constraint before honoring it with `git fetch origin main --quiet && git log <commit>..origin/main --oneline -- <the paths the constraint names>`. Any commit it reports means the track landed and the constraint is dead. Fetch inside the same command, since a remote-tracking ref left behind reports fewer merges than have landed and reads a dead constraint as live, which is the failure the stamp exists to remove.
|
|
79
|
+
- Scope the log to the paths rather than reading every subject. A squashed merge carries a pull request number while the constraint names its track by work and file set, so an unscoped log leaves the reader matching subjects.
|
|
80
|
+
- Read an unstamped constraint as unverified rather than as live. There is no commit to anchor the log against, so confirming one costs the open pull request list against the named paths, which is the work a stamp removes. Every plan written before this rule carries an unstamped block, which is why the default sits here rather than in a sweep over the live folder.
|
|
81
|
+
|
|
82
|
+
A dead constraint fails silently in the expensive direction. A session honoring one ships the dangling citation the change created and reports success, where a session crossing a live constraint collides visibly and is caught.
|
|
83
|
+
|
|
74
84
|
### Risks
|
|
75
85
|
|
|
76
86
|
- Name the collision rather than the category. A risk a reader cannot act on is padding.
|
|
@@ -141,6 +151,8 @@ This contract inverts the one an intake folder keeps, where an empty slot means
|
|
|
141
151
|
|
|
142
152
|
**Constraints:**
|
|
143
153
|
|
|
154
|
+
- Measured against `<commit>` on <YYYY-MM-DD>
|
|
155
|
+
- <the work in flight, its file set, and which of the two acts the constraint forbids>
|
|
144
156
|
- <durable rule the work must respect>
|
|
145
157
|
|
|
146
158
|
**Files to touch:**
|
package/standards/tasks.md
CHANGED
|
@@ -65,7 +65,7 @@ The `Waiting on` column under `## Up next` carries that reason in one of three f
|
|
|
65
65
|
|
|
66
66
|
Under `## Up next` a collision names the file held by the task already running, a sibling task names that task, and an external condition names both the condition and what would satisfy it. Naming what would satisfy it is what separates a blocked row from one nobody has examined, so a cell stating a condition with no way out of it fails the test. The header text is the contract the way the group names are, because anything reading the cell resolves the column by header rather than by position.
|
|
67
67
|
|
|
68
|
-
`aitk tasks validate` reads those columns and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a row and a task file that do not map one to one, a task in two groups, and two `## Run now` rows touching a path in common. It
|
|
68
|
+
`aitk tasks validate` reads those columns and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a row and a task file that do not map one to one, a task in two groups, and two `## Run now` rows touching a path in common. It also re-takes the two blocker kinds a command can settle, reporting a parked row whose cited task is archived or has closed every outcome and one whose cited file nothing under `## Run now` still holds. Both halves read a citation out of the cell rather than parsing it into fields, and a row citing neither is reported as untested, which is where the three kinds resting on a person's judgment land. Run it when the readiness claim is made rather than on a schedule, since the board is gitignored per-machine scratch and no shared moment exists to hang it on. It reports and never writes, so a session fixes the row it names.
|
|
69
69
|
|
|
70
70
|
```markdown
|
|
71
71
|
---
|