@erclx/aitk 0.97.0 → 0.98.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 +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +8 -8
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +7 -2
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +3 -1
- package/claude/skills/claude-orchestrate/scripts/poll.sh +84 -5
- package/claude/skills/claude-pr-review/SKILL.md +32 -12
- package/docs/agents/commands.md +1 -1
- package/docs/agents/tasks.md +30 -7
- package/docs/ai-workflow.md +2 -2
- package/docs/operating-model.md +10 -5
- package/package.json +1 -1
- package/src/commands/tasks.ts +19 -0
- package/src/tasks/validate.ts +169 -2
- package/standards/tasks.md +1 -1
|
@@ -109,7 +109,7 @@ Write no shape for a correction. A correction is a sentence, and a format for ad
|
|
|
109
109
|
5. Hand off. The human opens a worker worktree with `claude-worktree` and runs `claude-autoship` against the plan. The orchestrator does not spawn workers.
|
|
110
110
|
6. Review the PR. When a worker opens a PR, run `claude-pr-review` to post findings to it. This is the deep, independent pass. The worker's autoship self-review was only the green gate.
|
|
111
111
|
- Learning that a PR moved is the mechanical half, so read `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` and start the poll it carries on the first dispatch rather than checking the board by hand. That runbook holds the routing, and a summary of it here is a second source that drifts from it.
|
|
112
|
-
7. Dispatch the handback. A pass posting a
|
|
112
|
+
7. Dispatch the handback. A pass posting a finding at any severity tells the session holding that branch to run `claude-address-review`, rather than waiting for a person to relay it. Read that off the counts on the summary line rather than off the heading, since a minors-only pass closes the heading and still owes a dispatch. Re-review when the answer lands, then the human merges. Tell the trailing worker to rebase when its branch shares a seam with the merged one.
|
|
113
113
|
- Send only above that floor. A minor posts under the same heading and is visibility alone, so a rule keyed on any open finding sends a worker to act on a note
|
|
114
114
|
- Resolve the target from a fresh session listing at the moment of sending, never from a mapping written down earlier, since names rotate as sessions end and one recorded earlier in a session has failed inside the hour
|
|
115
115
|
- Open the message with the worktree and branch the sender believes the reader holds, asking to be corrected, whenever that mapping is inferred rather than confirmed
|
|
@@ -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.
|
|
@@ -34,12 +34,13 @@ Poll GitHub for pull request movement by running <POLL_SCRIPT>, then act on what
|
|
|
34
34
|
- MOVED or RESPONSE on a pull request I have already reviewed: run the aitk:claude-pr-review skill on it immediately, narrow pass. Re-reviews read prior..head and gain nothing from waiting.
|
|
35
35
|
- OPENED, or a pull request with no prior review pass: run the aitk:claude-pr-review skill on it. A draft counts, since every pull request here opens as one and skipping drafts skips everything.
|
|
36
36
|
- SEEN: report it and stop. A pass already covers that head, whether it arrived out of band or before the poll first saw the pull request, so no review follows.
|
|
37
|
+
- STALLED: read the last pass and report what it carried. The pass has sat open for hours with nothing following it, so a worker mid-task is already ruled out and the dispatch either never went out or the session holding it is gone. Confirm and re-send it under the dispatch rule below. A pass carrying only minors is also under the wrong heading, so say so, and dispatch it either way. Do not re-run a review to correct the heading, since a pass on an unchanged head with no response behind it stops by design.
|
|
37
38
|
- CONFLICT: report it and stop. The branch owner rebases, not this session.
|
|
38
39
|
- GONE: report it, then sweep the board by invoking the aitk:claude-orchestrate skill and following its queue-refill sweep.
|
|
39
40
|
- A line starting `poll:`: report it verbatim and treat that pull request as unread this run. It is a failed query, not a state.
|
|
40
41
|
- Nothing changed: say exactly "No movement." and nothing else.
|
|
41
42
|
|
|
42
|
-
After any pass that posts a
|
|
43
|
+
After any pass that posts a finding, at any severity, tell the session holding that branch to run the aitk:claude-address-review skill. Resolve the target by running `aitk sessions list --branch <branch> --json` at that moment, which scopes the match to this repository, then route on how many sessions it returned. Zero: report the invocation for me and dispatch nobody. Exactly one, with the confidence field reading "confirmed": address that name directly. Any other count, any other confidence, or a command that is missing or refuses: fall back to picking from a session listing, open by naming the worktree and branch you believe the reader holds, and ask to be corrected. Two sessions can hold one branch, so read the count rather than the first row. The heading does not decide this: a minors-only pass posts the closed heading and still owes a dispatch, so read the counts on the summary line rather than the heading above them.
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
## Reading the output
|
|
@@ -50,4 +51,8 @@ The script exits non-zero and classifies nothing when the open pull request list
|
|
|
50
51
|
|
|
51
52
|
The baseline lives at `.claude/.tmp/pr-poll/baseline.txt` under the main worktree root and is per-machine. A first run against a board already in flight reports each open pull request once before it settles.
|
|
52
53
|
|
|
53
|
-
The four review headings the script matches are written by `claude-pr-review` and `claude-address-review`. A project that posts its reviews under different headings edits the
|
|
54
|
+
The four review headings the script matches are written by `claude-pr-review` and `claude-address-review`. A project that posts its reviews under different headings edits the three jq filters in the script to match, or every pull request reads as never reviewed.
|
|
55
|
+
|
|
56
|
+
`STALLED` is the one state the script derives from a heading rather than from a commit or a count, since `claude-pr-review` posts the open heading only for a pass carrying a critical or a should-fix. The heading alone cannot carry it, because an open pass means a dispatch was owed and made, so the ordinary healthy thread is a worker still working. The age of that pass is the third test: the state fires when the open pass covers the head, nothing has followed it, and it is older than the `STALE_AFTER` seconds set at the top of the script. It reports once per entry and fires again after any commit or reply resets the thread. A project whose workers run longer than the default two hours raises that number.
|
|
57
|
+
|
|
58
|
+
What the state does not reach is a dispatch owed on a minor. The dispatch fires on a finding at any severity and the heading closes on a blocking one, so a minors-only pass owes a worker under a closed heading, and this state reads the open heading alone. It therefore catches a stalled blocking finding and never a stalled minor. That is a narrowing rather than a defect, and closing it would mean reading the counts off the summary line, which pins a second string this script does not own.
|
|
@@ -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
|
|
|
@@ -55,6 +55,44 @@ JQ_REPLY_COUNT='
|
|
|
55
55
|
] | length
|
|
56
56
|
'
|
|
57
57
|
|
|
58
|
+
# `claude-pr-review` posts `## Review` only for a pass carrying a critical or a
|
|
59
|
+
# should-fix, so the heading of the last review is what says whether anything
|
|
60
|
+
# blocks the merge. Taking it as well as the commit is what separates a thread
|
|
61
|
+
# waiting on a worker from one a pass left open owing no dispatch. A project
|
|
62
|
+
# editing the two filters above for its own headings edits this one with them.
|
|
63
|
+
#
|
|
64
|
+
# The age of that review comes out of the same selection, because the heading
|
|
65
|
+
# alone cannot separate the two. Under the rule above an open heading means a
|
|
66
|
+
# dispatch was owed and made, so the ordinary healthy thread is a worker still
|
|
67
|
+
# working and every one of them would be reported minutes after the pass posted.
|
|
68
|
+
# jq computes the elapsed seconds itself, since `date -d` is GNU-only and this
|
|
69
|
+
# script runs wherever the plugin is installed. A review carrying no stamp reads
|
|
70
|
+
# as age zero and classifies nothing, which is the same answer the carry-forward
|
|
71
|
+
# path gives a pull request this run could not read.
|
|
72
|
+
JQ_LAST_REVIEW_STATE='
|
|
73
|
+
[ .reviews[]
|
|
74
|
+
| select((.body // "") | split("\n")[0] | rtrimstr("\r")
|
|
75
|
+
| . == "## Review" or . == "## Review closed")
|
|
76
|
+
] | last
|
|
77
|
+
| if . == null then "none 0"
|
|
78
|
+
else ((.body | split("\n")[0] | rtrimstr("\r")
|
|
79
|
+
| if . == "## Review" then "open" else "closed" end)
|
|
80
|
+
+ " "
|
|
81
|
+
+ (if .submittedAt == null then "0"
|
|
82
|
+
else ((now - (.submittedAt | fromdateiso8601)) | floor | tostring)
|
|
83
|
+
end))
|
|
84
|
+
end
|
|
85
|
+
'
|
|
86
|
+
|
|
87
|
+
# An open pass this old has nobody on it. The cycle it has to clear is a review
|
|
88
|
+
# landing and a worker pushing a follow-up, measured between ten and thirty
|
|
89
|
+
# minutes across a day of runs on 2026-08-14, so two hours sits about four times
|
|
90
|
+
# past the slowest observed and still well inside a working session. Shorter
|
|
91
|
+
# starts catching the slow healthy case this threshold exists to exclude, and
|
|
92
|
+
# longer lets a dead dispatch sit past the point a person would have noticed.
|
|
93
|
+
# A project whose workers run longer than that raises it here.
|
|
94
|
+
STALE_AFTER=7200
|
|
95
|
+
|
|
58
96
|
# A pull request this run could not read keeps the line it had, so the GONE
|
|
59
97
|
# sweep below does not read the gap as a merge and the baseline does not lose
|
|
60
98
|
# the head it already knew. Saying so on stderr is the point: a silent skip is
|
|
@@ -63,7 +101,14 @@ carry_forward() {
|
|
|
63
101
|
local n=$1 reason=$2 old
|
|
64
102
|
old=$(grep "^$n " "$STATE" || true)
|
|
65
103
|
if [ -n "$old" ]; then
|
|
66
|
-
|
|
104
|
+
# Every other classification compares a field against itself on a carried
|
|
105
|
+
# line, so it fires nothing. STALLED reads a heading that is carried too, so
|
|
106
|
+
# the field is blanked to a value no branch matches. Echoing it intact would
|
|
107
|
+
# classify a pull request this run never reached while stderr below says the
|
|
108
|
+
# opposite, and one successful read restores it a run later. An already
|
|
109
|
+
# reported thread keeps its marker, or an unreadable run would let the same
|
|
110
|
+
# thread be reported a second time.
|
|
111
|
+
echo "$old" | awk '{ if ($6 != "reported") $6 = "carried"; print }'
|
|
67
112
|
echo "poll: #$n $reason, so it keeps its last known state and goes unclassified" >&2
|
|
68
113
|
else
|
|
69
114
|
echo "poll: #$n $reason, and it has no last known state, so it goes unclassified" >&2
|
|
@@ -71,7 +116,7 @@ carry_forward() {
|
|
|
71
116
|
}
|
|
72
117
|
|
|
73
118
|
snapshot() {
|
|
74
|
-
local numbers n payload head prior resp merges
|
|
119
|
+
local numbers n payload head prior resp merges review_state
|
|
75
120
|
git fetch -q origin "$BASE_BRANCH" 2>/dev/null || true
|
|
76
121
|
|
|
77
122
|
# A failed list reaches the caller as no open pull requests, and that reports
|
|
@@ -102,6 +147,9 @@ snapshot() {
|
|
|
102
147
|
|
|
103
148
|
prior=$(jq -r "$JQ_LAST_REVIEWED_HEAD" <<<"$payload")
|
|
104
149
|
resp=$(jq -r "$JQ_REPLY_COUNT" <<<"$payload")
|
|
150
|
+
# Two space-separated fields, so the line below carries them as its own
|
|
151
|
+
# sixth and seventh rather than needing a split.
|
|
152
|
+
review_state=$(jq -r "$JQ_LAST_REVIEW_STATE" <<<"$payload")
|
|
105
153
|
|
|
106
154
|
# `gh pr view --json mergeable` reports UNKNOWN until GitHub finishes
|
|
107
155
|
# computing it, which is exactly when a poll asks. merge-tree answers
|
|
@@ -120,7 +168,7 @@ snapshot() {
|
|
|
120
168
|
merges=conflict
|
|
121
169
|
fi
|
|
122
170
|
|
|
123
|
-
echo "$n $head ${prior:-none} $resp $merges"
|
|
171
|
+
echo "$n $head ${prior:-none} $resp $merges $review_state"
|
|
124
172
|
done
|
|
125
173
|
}
|
|
126
174
|
|
|
@@ -131,9 +179,15 @@ if ! NEW=$(snapshot); then
|
|
|
131
179
|
exit 1
|
|
132
180
|
fi
|
|
133
181
|
CHANGED=0
|
|
182
|
+
# The baseline is written from here rather than from the snapshot, because the
|
|
183
|
+
# heading field is the one column a classification rewrites. STALLED reports a
|
|
184
|
+
# standing condition rather than a transition, so without a written marker it
|
|
185
|
+
# would fire on every later run and the board would never read "No movement."
|
|
186
|
+
FINAL=""
|
|
134
187
|
|
|
135
|
-
while read -r n head prior resp merges; do
|
|
188
|
+
while read -r n head prior resp merges heading age; do
|
|
136
189
|
[ -z "$n" ] && continue
|
|
190
|
+
state=$heading
|
|
137
191
|
old=$(grep "^$n " "$STATE" || true)
|
|
138
192
|
if [ -z "$old" ]; then
|
|
139
193
|
# A pull request first seen here may already carry a pass, when it opened
|
|
@@ -149,11 +203,17 @@ while read -r n head prior resp merges; do
|
|
|
149
203
|
echo "OPENED #$n at ${head:0:7}, $merges against $BASE_BRANCH"
|
|
150
204
|
fi
|
|
151
205
|
CHANGED=1
|
|
206
|
+
FINAL+="$n $head $prior $resp $merges $state $age"$'\n'
|
|
152
207
|
continue
|
|
153
208
|
fi
|
|
154
209
|
old_head=$(echo "$old" | cut -d' ' -f2)
|
|
155
210
|
old_resp=$(echo "$old" | cut -d' ' -f4)
|
|
156
211
|
old_merges=$(echo "$old" | cut -d' ' -f5)
|
|
212
|
+
# Only the marker is read off this field, so a baseline written before it
|
|
213
|
+
# existed yields empty and reads as not yet reported, which is what it is. The
|
|
214
|
+
# age decides the rest, so the first run after an upgrade needs no history and
|
|
215
|
+
# classifies a thread already past the threshold rather than waiting a run.
|
|
216
|
+
old_heading=$(echo "$old" | cut -d' ' -f6)
|
|
157
217
|
|
|
158
218
|
# A conflict arrives from the base moving, not from the branch, so it is
|
|
159
219
|
# reported on the transition rather than only when the head changes.
|
|
@@ -187,7 +247,26 @@ while read -r n head prior resp merges; do
|
|
|
187
247
|
elif [ "$resp" -gt "$old_resp" ]; then
|
|
188
248
|
echo "RESPONSE #$n answered with no new commit"
|
|
189
249
|
CHANGED=1
|
|
250
|
+
elif [ "$heading" = open ] && [ "$old_heading" != reported ] &&
|
|
251
|
+
[ "$prior" = "$head" ] && [ "$age" -ge "$STALE_AFTER" ]; then
|
|
252
|
+
# Three things at once: the last pass is open, it covers the head so no
|
|
253
|
+
# commit followed it, and the two branches above found no reply either. The
|
|
254
|
+
# open heading is posted only for a critical or should-fix, so a pass owing
|
|
255
|
+
# no dispatch left it here. Any two of these describe an ordinary review
|
|
256
|
+
# waiting on a worker, which is why the age carries the third: without it
|
|
257
|
+
# every dispatched worker is reported minutes into the work it was sent to
|
|
258
|
+
# do, and a signal firing on the healthy path is one an operator learns to
|
|
259
|
+
# skip. The report still asks for a confirmation rather than asserting.
|
|
260
|
+
echo "STALLED #$n open at ${head:0:7}, no commit or reply in $((age / 3600))h"
|
|
261
|
+
state=reported
|
|
262
|
+
CHANGED=1
|
|
263
|
+
elif [ "$heading" = open ] && [ "$old_heading" = reported ]; then
|
|
264
|
+
# Nothing above fired, so the thread has not moved since it was reported.
|
|
265
|
+
# Carry the marker rather than the freshly derived heading, or the state
|
|
266
|
+
# re-enters next run and STALLED oscillates instead of reporting once.
|
|
267
|
+
state=reported
|
|
190
268
|
fi
|
|
269
|
+
FINAL+="$n $head $prior $resp $merges $state"$'\n'
|
|
191
270
|
done <<<"$NEW"
|
|
192
271
|
|
|
193
272
|
while read -r n _rest; do
|
|
@@ -199,4 +278,4 @@ while read -r n _rest; do
|
|
|
199
278
|
done <"$STATE"
|
|
200
279
|
|
|
201
280
|
[ "$CHANGED" -eq 0 ] && echo "No movement."
|
|
202
|
-
|
|
281
|
+
printf '%s' "$FINAL" >"$STATE"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-pr-review
|
|
3
|
-
description: Reviews an open pull request from an independent session and posts findings as a review comment on the PR. Posts a first pass against the whole change and every later pass against only the commits added since, under `## Review` while
|
|
3
|
+
description: Reviews an open pull request from an independent session and posts findings as a review comment on the PR. Posts a first pass against the whole change and every later pass against only the commits added since, under `## Review` while a critical or should-fix is open and `## Review closed` once nothing blocks the merge. Reads project docs and the roadmap for cross-feature context a self-review lacks. Use when asked to "review the PR", "review this feature's PR", "post a PR review", "re-review the PR", "close out the review", "confirm the findings are fixed", or acting as the orchestrator reviewing a worker's PR. Do NOT use to review local uncommitted changes. That is `claude-review`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Claude PR review
|
|
@@ -13,10 +13,10 @@ findings to the PR, so the vantage is independent and the output is durable.
|
|
|
13
13
|
It posts at least twice over a pull request's life. A first pass opens the
|
|
14
14
|
review against the whole change, and every later pass reads only the commits
|
|
15
15
|
added since. The heading reports state rather than pass number: a pass carrying
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
thread rather than named by the caller.
|
|
16
|
+
a critical or a should-fix takes `## Review`, and `## Review closed` covers
|
|
17
|
+
every other pass, so the most recent comment's heading reports whether anything
|
|
18
|
+
blocks the merge. Every pass is this skill, and which one it is gets detected
|
|
19
|
+
from the thread rather than named by the caller.
|
|
20
20
|
|
|
21
21
|
## Guards
|
|
22
22
|
|
|
@@ -85,9 +85,9 @@ Review the diff and files for the same axes as `claude-review` (bugs, edge cases
|
|
|
85
85
|
|
|
86
86
|
Apply the high-signal filter: flag only what will cause incorrect behavior, break a documented rule, or mislead a downstream feature. If uncertain, do not flag.
|
|
87
87
|
|
|
88
|
-
A later pass applies the same axes to the delta, and adds one check the first pass cannot make: did each prior finding land, and did the fix regress anything it touched. Findings of its own are normal findings, stated at the same severity and counted the same way. That count is what Step 4 reads to pick the heading, so a pass raising
|
|
88
|
+
A later pass applies the same axes to the delta, and adds one check the first pass cannot make: did each prior finding land, and did the fix regress anything it touched. Findings of its own are normal findings, stated at the same severity and counted the same way. That count is what Step 4 reads to pick the heading, so a pass raising a critical or a should-fix of its own is not a close-out.
|
|
89
89
|
|
|
90
|
-
A prior finding can also be settled by argument rather than by a fix. A reply naming the plan question that already declined it, or a constraint this session could not see, withdraws the finding or moves its grade. State that outcome in the body under the finding it changes, naming the fact that produced it, whether the argument arrived on the thread or through the channel that carried the dispatch. Dropping the finding from this body instead leaves a reader unable to tell a withdrawal from an oversight, and the reasoning goes with the session that heard it. A withdrawal removes the finding from the count, so a pass that withdrew every
|
|
90
|
+
A prior finding can also be settled by argument rather than by a fix. A reply naming the plan question that already declined it, or a constraint this session could not see, withdraws the finding or moves its grade. State that outcome in the body under the finding it changes, naming the fact that produced it, whether the argument arrived on the thread or through the channel that carried the dispatch. Dropping the finding from this body instead leaves a reader unable to tell a withdrawal from an oversight, and the reasoning goes with the session that heard it. A withdrawal removes the finding from the count, so a pass that withdrew every critical and should-fix is a close-out. Write the withdrawal and its cause into that body rather than taking the short close-out line Step 4 supplies, which reports prior findings addressed and would credit a fix nobody made.
|
|
91
91
|
|
|
92
92
|
Use severity: `critical` (blocks merge), `should-fix` (fix before merge), `minor` (visibility only).
|
|
93
93
|
|
|
@@ -144,9 +144,15 @@ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-
|
|
|
144
144
|
🤖 Reviewed by Claude Code
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
The heading reports whether anything
|
|
147
|
+
The heading reports whether anything blocks the merge and the summary line reports which pass this is. Post under `## Review` whenever the pass carries a critical or a should-fix, whether it is the first pass or the fourth. Post `## Review closed` on every other pass, a pass carrying only minors included, and list those minors under it as follow-ups. A pull request thread then reads as `## Review`, the worker's answer under `## Review response` from `claude-address-review`, another `## Review` while a critical or should-fix is still open, and `## Review closed` when neither is.
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
The heading and the dispatch answer two different questions, so a close-out does not mean the pass owes nobody. The heading says whether anything blocks the merge and the dispatch says whether any work is owed, and a pass carrying only minors closes the heading while still sending the session holding the branch. Keying the dispatch on the grade too was measured wrong: across 8 findings on one archived pass, 3 were posted as minor and 2 of those were defects a worker fixed rather than recorded, so a floor at should-fix loses real fixes to a grade that runs low.
|
|
150
|
+
|
|
151
|
+
The cost is that a clean thread no longer reads off the heading alone, since a close-out may carry minors. Take the merge decision from the heading and the counts from the summary line under it, which is where they already sit.
|
|
152
|
+
|
|
153
|
+
A minor the dispatched worker declines is what needs a surface that survives the merge, rather than every minor, since one that gets fixed on the branch needs no durable record. Write a declined minor into the `## Findings` section of the task the branch closes, which is where the queue-refill sweep already routes a finding that changes another task. A declined finding left on the thread alone is lost the moment the pull request merges.
|
|
154
|
+
|
|
155
|
+
Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises a critical or a should-fix.
|
|
150
156
|
|
|
151
157
|
Both of this skill's headings anchor as a section distinct from human threads. Do not invent one beyond those two and the `## Review response` a sibling owns, and do not append the PR number, which GitHub already renders above the comment.
|
|
152
158
|
|
|
@@ -166,11 +172,25 @@ Before posting, run the scan in `.claude/standards/publish.md` against the body,
|
|
|
166
172
|
gh pr review <number> --comment --body-file .claude/.tmp/pr-review/body-<number>-<short-sha>.md
|
|
167
173
|
```
|
|
168
174
|
|
|
169
|
-
A pass
|
|
175
|
+
A pass carrying nothing at all takes `## Review closed` and a short body, with the footer line included either way. On a first pass, post `✅ No blocking findings. Reviewed against project docs and roadmap.` On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
|
|
176
|
+
|
|
177
|
+
A pass carrying only minors takes the same heading and the full shape rather than either short line, since the minors have to be readable and neither line reports them. Keep whichever scope sentence the pass owes on the summary line:
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
## Review closed
|
|
181
|
+
|
|
182
|
+
0 critical, 0 should-fix, Z minor. Nothing blocks the merge. Reviewed against project docs and roadmap.
|
|
183
|
+
|
|
184
|
+
**`path/to/file.ext`**
|
|
185
|
+
|
|
186
|
+
- **minor**: finding, and the fix it wants.
|
|
187
|
+
|
|
188
|
+
🤖 Reviewed by Claude Code
|
|
189
|
+
```
|
|
170
190
|
|
|
171
|
-
A pass that closed by withdrawing a finding rather than by reading its fix takes neither line, per the withdrawal rule in Step 3. Both claim a fix landed, and the second names it, so posting either over a withdrawal credits work nobody did on the one comment a reader treats as the verdict. Write the withdrawal and the fact that settled it in place of the canned line, keeping the heading and the footer.
|
|
191
|
+
A pass that closed by withdrawing a finding rather than by reading its fix takes neither ✅ line, per the withdrawal rule in Step 3. Both claim a fix landed, and the second names it, so posting either over a withdrawal credits work nobody did on the one comment a reader treats as the verdict. Write the withdrawal and the fact that settled it in place of the canned line, keeping the heading and the footer.
|
|
172
192
|
|
|
173
|
-
Post
|
|
193
|
+
Post a close-out even when there is nothing to report. A review left with no closing comment reads as one nobody answered.
|
|
174
194
|
|
|
175
195
|
## Step 5: output
|
|
176
196
|
|
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
|
@@ -73,7 +73,7 @@ When features are independent, run them in parallel instead of sequentially. Use
|
|
|
73
73
|
- Ship each worktree separately with `aitk:git-ship`
|
|
74
74
|
- For full autonomy per worktree, invoke `aitk:claude-autoship` instead of the manual chain. Approve the plan, walk away, come back to draft PRs.
|
|
75
75
|
|
|
76
|
-
To run several worktrees as a coordinated flow rather than ad hoc, assert the orchestrator role in one warm session with `aitk:claude-orchestrate`. It owns the roadmap, handing a needed draft or resequence to a worker that runs `aitk:claude-roadmap` in its branch, plans each feature, refills the ready queue so a free worker never waits, and reviews each worker's PR with `aitk:claude-pr-review`, then tells the session holding that branch to run `aitk:claude-address-review` whenever the pass posted a
|
|
76
|
+
To run several worktrees as a coordinated flow rather than ad hoc, assert the orchestrator role in one warm session with `aitk:claude-orchestrate`. It owns the roadmap, handing a needed draft or resequence to a worker that runs `aitk:claude-roadmap` in its branch, plans each feature, refills the ready queue so a free worker never waits, and reviews each worker's PR with `aitk:claude-pr-review`, then tells the session holding that branch to run `aitk:claude-address-review` whenever the pass posted a finding at any severity, read off the counts on the summary line rather than off the heading above them. The human launches workers and merges. See [operating model](operating-model.md) for the full loop.
|
|
77
77
|
|
|
78
78
|
Roadmap ownership holds while a scope exists to sequence. Once the MVP list in `.claude/REQUIREMENTS.md` has shipped, later work arrives as discrete items and the orchestrator reads `.claude/tasks/priority.md` for execution order instead.
|
|
79
79
|
|
|
@@ -103,7 +103,7 @@ The sweep reads the whole board rather than the tasks the session touched. It is
|
|
|
103
103
|
|
|
104
104
|
`.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
105
|
|
|
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
|
|
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 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
107
|
|
|
108
108
|
`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
109
|
|
package/docs/operating-model.md
CHANGED
|
@@ -40,8 +40,8 @@ One feature travels this path end to end.
|
|
|
40
40
|
2. Orchestrator plans the next feature with `claude-feature`, writing a plan to `.claude/plans/`. Planning stays in the warm session because good planning is cross-feature. It needs the contract other features consume and the shared wiring seam. A cold session would re-derive or guess.
|
|
41
41
|
3. The human opens a worker worktree with `claude-worktree` and runs `claude-autoship` against the plan. The worker builds, self-checks, opens a PR, and stops at the PR boundary.
|
|
42
42
|
4. Orchestrator reviews the PR with `claude-pr-review` and posts findings to it.
|
|
43
|
-
5. Orchestrator tells the session holding that branch to run `claude-address-review` once the pass posted a
|
|
44
|
-
6. Orchestrator closes the review out with `claude-pr-review` again. The second pass reads only the commits the follow-up added, or the worker's response alone when the follow-up added none, and posts under `## Review
|
|
43
|
+
5. Orchestrator tells the session holding that branch to run `claude-address-review` once the pass posted a finding at any severity, resolving the target from a session listing taken at that moment and reporting the invocation for the human when no live session holds it. The worker addresses the findings, rebases onto `origin/main` when a sibling landed first and left the branch unable to merge, then pushes a follow-up. A pass carrying only minor findings dispatches too, since the grade runs low often enough that a floor at should-fix loses fixes a worker would have made.
|
|
44
|
+
6. Orchestrator closes the review out with `claude-pr-review` again. The second pass reads only the commits the follow-up added, or the worker's response alone when the follow-up added none, and posts under `## Review` when it finds a critical or a should-fix and under `## Review closed` otherwise, so a reader learns the merge decision from the heading and the counts from the line under it. Repeat from step 5 until a pass closes the review.
|
|
45
45
|
7. The human reads the result and merges. The orchestrator tells any trailing worker whose branch shares a seam with the merged one to run `claude-address-review`, which rebases whether or not the review left anything open.
|
|
46
46
|
|
|
47
47
|
There is no loop construct here. Each worker is a single build that halts at the
|
|
@@ -102,9 +102,14 @@ closed clean and then went stale still rebases when the skill is invoked. The
|
|
|
102
102
|
re-read costs a full pass rather than a delta, since the prior reviewed commit no
|
|
103
103
|
longer reaches the head, and `claude-pr-review` detects that itself.
|
|
104
104
|
|
|
105
|
-
The heading carries the state rather than the pass number. A pass
|
|
106
|
-
|
|
107
|
-
be scanned for what
|
|
105
|
+
The heading carries the state rather than the pass number. A pass carrying a
|
|
106
|
+
critical or a should-fix takes `## Review` and every other pass takes
|
|
107
|
+
`## Review closed`, so a thread can be scanned for what still blocks a merge
|
|
108
|
+
without opening a comment. The heading and the dispatch answer two questions
|
|
109
|
+
rather than one: a minors-only pass closes the heading and still sends a worker,
|
|
110
|
+
so the dispatch reads the counts on the summary line rather than the heading
|
|
111
|
+
above them. A minor the worker declines goes to the findings of the task the
|
|
112
|
+
branch closes, since a thread does not survive the merge. The feedback
|
|
108
113
|
becomes a durable artifact both sessions read, survives a session ending, and
|
|
109
114
|
anchors to the change. That removes the copy-paste that otherwise routes review
|
|
110
115
|
through the human between two sessions.
|
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/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
|
---
|