@mmerterden/multi-agent-pipeline 20.37.0 → 20.37.1
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/CHANGELOG.md +25 -0
- package/docs/facts.json +2 -2
- package/docs/features.md +1 -1
- package/docs/recovery-guide.md +1 -1
- package/install/catalog-history.json +1 -1
- package/manifest.json +40 -35
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +12 -7
- package/pipeline/multi-agent-refs/audit-guide.md +20 -0
- package/pipeline/multi-agent-refs/features/commit-multi-repo.md +63 -0
- package/pipeline/multi-agent-refs/features/dev-multi-repo.md +51 -0
- package/pipeline/multi-agent-refs/features/test-gap.md +40 -0
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/orchestrator/phase-3-user-test.md +12 -9
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +8 -47
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +21 -65
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +9 -59
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +0 -31
- package/pipeline/multi-agent-refs/rules.md +1 -1
- package/pipeline/office/index.html +6 -3
- package/pipeline/office/toolkit.json +1 -1
- package/pipeline/rules/figma-pipeline.md +6 -3
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/scripts/_memory-policy.mjs +27 -5
- package/pipeline/scripts/checkpoint-fold.sh +59 -0
- package/pipeline/scripts/gate-ledger.mjs +1 -1
- package/pipeline/scripts/office-control.mjs +41 -15
- package/pipeline/scripts/offload-ref.sh +1 -1
- package/pipeline/scripts/rework-rulings.mjs +5 -3
- package/pipeline/scripts/scope-check-gate.mjs +15 -2
- package/pipeline/scripts/user-test-handoff.sh +203 -0
- package/pipeline/scripts/validate-planning.mjs +5 -3
- package/pipeline/scripts/validate-triage.mjs +2 -1
- package/pipeline/scripts/write-state.mjs +13 -4
- package/pipeline/skills/.skill-manifest.json +2 -2
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +12 -7
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# user-test-handoff.sh - hand a task branch from its worktree to the main
|
|
4
|
+
# repository for a manual test, and hand it back.
|
|
5
|
+
#
|
|
6
|
+
# A branch can be checked out in one place only, so testing in the main repo
|
|
7
|
+
# means removing the worktree first. That removal is where work gets lost: an
|
|
8
|
+
# uncommitted change, a submodule's local branches and stashes (its git dir
|
|
9
|
+
# lives inside the worktree's admin dir), and every ignored file (the run
|
|
10
|
+
# artefacts under `.pipeline/` and `.multi-agent/`, logs, `.env`, build
|
|
11
|
+
# output) live only in the worktree. This script refuses what it cannot carry
|
|
12
|
+
# and carries the rest.
|
|
13
|
+
#
|
|
14
|
+
# It never commits. The caller makes the WIP commit itself first, so the
|
|
15
|
+
# commit passes through the same approval and secret-scan hooks as any other.
|
|
16
|
+
#
|
|
17
|
+
# checkout check that the worktree and its submodules hold no uncommitted
|
|
18
|
+
# or unpushed work, move every ignored entry and the main repo's
|
|
19
|
+
# current branch into a hold under the git common dir, remove the
|
|
20
|
+
# worktree, check the branch out in the main repo.
|
|
21
|
+
# restore put the main repo back on the branch it was on, re-add the
|
|
22
|
+
# worktree on the task branch, move the held entries back. Safe to
|
|
23
|
+
# run again after an interrupted checkout or restore.
|
|
24
|
+
#
|
|
25
|
+
# The hold sits in the same repository as the worktree, so moving even a large
|
|
26
|
+
# ignored tree is a rename. Success is judged by where the branch ends up, not
|
|
27
|
+
# by git's exit code: a failing post-checkout hook (git LFS, core.hooksPath)
|
|
28
|
+
# makes `git checkout` and `git worktree add` exit non-zero after they did
|
|
29
|
+
# their work. The hold is deleted only once every entry is back.
|
|
30
|
+
#
|
|
31
|
+
# Usage:
|
|
32
|
+
# user-test-handoff.sh checkout|restore --task <id> --worktree <path>
|
|
33
|
+
# --root <main repo> --branch <task branch>
|
|
34
|
+
#
|
|
35
|
+
# Prints one JSON line on success. Exit codes:
|
|
36
|
+
# 0 done
|
|
37
|
+
# 1 usage error, or the worktree is not on the task branch
|
|
38
|
+
# 3 the main repo has uncommitted changes to tracked files; nothing moved
|
|
39
|
+
# 5 the worktree or a submodule has uncommitted, unpushed or stashed work;
|
|
40
|
+
# nothing moved
|
|
41
|
+
# 6 checkout: a handoff is already held (run restore), or git refused and
|
|
42
|
+
# everything was put back (restore then answers 7). restore: the main repo
|
|
43
|
+
# could not leave the branch, or the worktree could not be re-added; the
|
|
44
|
+
# hold is kept and the message names the cause
|
|
45
|
+
# 7 restore found nothing held for this task
|
|
46
|
+
set -uo pipefail
|
|
47
|
+
|
|
48
|
+
usage() {
|
|
49
|
+
echo "usage: user-test-handoff.sh checkout|restore --task <id> --worktree <path> --root <repo> --branch <name>" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
MODE="${1:-}"
|
|
54
|
+
[ $# -gt 0 ] && shift
|
|
55
|
+
TASK="" WT="" ROOT="" BRANCH=""
|
|
56
|
+
while [ $# -gt 0 ]; do
|
|
57
|
+
[ $# -ge 2 ] || usage
|
|
58
|
+
case "$1" in
|
|
59
|
+
--task) TASK="$2" ;;
|
|
60
|
+
--worktree) WT="$2" ;;
|
|
61
|
+
--root) ROOT="$2" ;;
|
|
62
|
+
--branch) BRANCH="$2" ;;
|
|
63
|
+
*) echo "user-test-handoff: unknown argument $1" >&2; usage ;;
|
|
64
|
+
esac
|
|
65
|
+
shift 2
|
|
66
|
+
done
|
|
67
|
+
case "$MODE" in checkout | restore) ;; *) usage ;; esac
|
|
68
|
+
[ -n "$TASK" ] && [ -n "$WT" ] && [ -n "$ROOT" ] && [ -n "$BRANCH" ] || usage
|
|
69
|
+
case "$TASK" in */* | *..* | .* | *'"'* | *\\*) echo "user-test-handoff: task id must be a plain name" >&2; exit 1 ;; esac
|
|
70
|
+
|
|
71
|
+
ROOT="$(cd "$ROOT" 2>/dev/null && pwd)" || { echo "user-test-handoff: --root is not a directory" >&2; exit 1; }
|
|
72
|
+
case "$WT" in /*) ;; *) WT="$ROOT/$WT" ;; esac
|
|
73
|
+
COMMON="$(git -C "$ROOT" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" ||
|
|
74
|
+
{ echo "user-test-handoff: --root is not a git repository" >&2; exit 1; }
|
|
75
|
+
HOLD="$COMMON/multi-agent-user-test/$TASK"
|
|
76
|
+
|
|
77
|
+
esc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
|
78
|
+
json() { printf '{"status":"%s","task":"%s","branch":"%s","previous":"%s"}\n' "$1" "$(esc "$TASK")" "$(esc "$BRANCH")" "$(esc "$2")"; }
|
|
79
|
+
|
|
80
|
+
on_branch() { [ "$(git -C "$1" branch --show-current 2>/dev/null)" = "$2" ]; }
|
|
81
|
+
|
|
82
|
+
root_dirty() { [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no 2>/dev/null)" ]; }
|
|
83
|
+
|
|
84
|
+
# Every ignored entry of the worktree, directories collapsed, NUL-separated.
|
|
85
|
+
ignored_entries() { git -C "$WT" ls-files --others --ignored --exclude-standard --directory -z 2>/dev/null; }
|
|
86
|
+
|
|
87
|
+
# Move the listed entries (NUL-separated file $3) from tree $1 to tree $2.
|
|
88
|
+
move_entries() {
|
|
89
|
+
local from="$1" to="$2" list="$3" e rc=0
|
|
90
|
+
while IFS= read -r -d '' e; do
|
|
91
|
+
e="${e%/}"
|
|
92
|
+
[ -n "$e" ] && [ -e "$from/$e" ] || continue
|
|
93
|
+
mkdir -p "$to/$(dirname "$e")" && rm -rf "${to:?}/$e" && mv "$from/$e" "$to/$e" || rc=1
|
|
94
|
+
done <"$list"
|
|
95
|
+
return "$rc"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return_entries() {
|
|
99
|
+
[ -f "$HOLD/entries" ] || : >"$HOLD/entries"
|
|
100
|
+
if move_entries "$HOLD/tree" "$WT" "$HOLD/entries"; then
|
|
101
|
+
rm -rf "$HOLD"
|
|
102
|
+
else
|
|
103
|
+
echo "user-test-handoff: some held entries are still under $HOLD/tree" >&2
|
|
104
|
+
fi
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
submodule_problems() {
|
|
108
|
+
# shellcheck disable=SC2016 # submodule foreach expands $displaypath itself
|
|
109
|
+
git -C "$WT" submodule foreach --quiet --recursive '
|
|
110
|
+
if [ -n "$(git status --porcelain)" ]; then echo "$displaypath (uncommitted changes)"
|
|
111
|
+
elif ! git branch -r --contains HEAD 2>/dev/null | grep -q .; then echo "$displaypath (HEAD on no remote branch; push it, or add a remote if there is none)"
|
|
112
|
+
elif [ -n "$(git rev-list -n1 --branches --not --remotes 2>/dev/null)" ]; then echo "$displaypath (a local branch has unpushed commits)"
|
|
113
|
+
elif [ -n "$(git stash list 2>/dev/null)" ]; then echo "$displaypath (has stashes)"
|
|
114
|
+
fi' 2>/dev/null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
checkout() {
|
|
118
|
+
on_branch "$WT" "$BRANCH" || { echo "user-test-handoff: $WT is not a worktree on $BRANCH" >&2; exit 1; }
|
|
119
|
+
if [ -e "$HOLD" ]; then
|
|
120
|
+
echo "user-test-handoff: a handoff for $TASK is already held; run restore first" >&2
|
|
121
|
+
exit 6
|
|
122
|
+
fi
|
|
123
|
+
if root_dirty; then
|
|
124
|
+
echo "user-test-handoff: the main repo has uncommitted changes; commit or stash them, then retry" >&2
|
|
125
|
+
git -C "$ROOT" status --short --untracked-files=no >&2
|
|
126
|
+
exit 3
|
|
127
|
+
fi
|
|
128
|
+
local problems
|
|
129
|
+
problems="$(submodule_problems)"
|
|
130
|
+
if [ -n "$problems" ]; then
|
|
131
|
+
printf 'user-test-handoff: these submodules hold work the removal would lose:\n%s\n' "$problems" >&2
|
|
132
|
+
exit 5
|
|
133
|
+
fi
|
|
134
|
+
if [ -n "$(git -C "$WT" status --porcelain)" ]; then
|
|
135
|
+
echo "user-test-handoff: the worktree has uncommitted changes; make the WIP commit first" >&2
|
|
136
|
+
git -C "$WT" status --short >&2
|
|
137
|
+
exit 5
|
|
138
|
+
fi
|
|
139
|
+
local previous
|
|
140
|
+
previous="$(git -C "$ROOT" symbolic-ref --short -q HEAD || git -C "$ROOT" rev-parse HEAD)"
|
|
141
|
+
mkdir -p "$HOLD/tree" || exit 6
|
|
142
|
+
printf '%s\n' "$previous" >"$HOLD/previous" || exit 6
|
|
143
|
+
ignored_entries >"$HOLD/entries" || exit 6
|
|
144
|
+
if ! move_entries "$WT" "$HOLD/tree" "$HOLD/entries"; then
|
|
145
|
+
return_entries
|
|
146
|
+
exit 6
|
|
147
|
+
fi
|
|
148
|
+
if ! git -C "$ROOT" worktree remove --force "$WT"; then
|
|
149
|
+
return_entries
|
|
150
|
+
exit 6
|
|
151
|
+
fi
|
|
152
|
+
git -C "$ROOT" checkout -q "$BRANCH" || true
|
|
153
|
+
if ! on_branch "$ROOT" "$BRANCH"; then
|
|
154
|
+
git -C "$ROOT" worktree add -q "$WT" "$BRANCH" || true
|
|
155
|
+
on_branch "$WT" "$BRANCH" && return_entries
|
|
156
|
+
echo "user-test-handoff: the main repo could not check out $BRANCH" >&2
|
|
157
|
+
exit 6
|
|
158
|
+
fi
|
|
159
|
+
json checked-out "$previous"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
restore() {
|
|
163
|
+
[ -f "$HOLD/previous" ] || { echo "user-test-handoff: nothing is held for $TASK" >&2; exit 7; }
|
|
164
|
+
local previous
|
|
165
|
+
previous="$(cat "$HOLD/previous")"
|
|
166
|
+
if on_branch "$ROOT" "$BRANCH"; then
|
|
167
|
+
if root_dirty; then
|
|
168
|
+
echo "user-test-handoff: the main repo has uncommitted changes on $BRANCH; stash them (git stash), restore, then git stash pop in the worktree" >&2
|
|
169
|
+
git -C "$ROOT" status --short --untracked-files=no >&2
|
|
170
|
+
exit 3
|
|
171
|
+
fi
|
|
172
|
+
if [ -n "$previous" ] && [ "$previous" != "$BRANCH" ]; then
|
|
173
|
+
git -C "$ROOT" checkout -q "$previous" || true
|
|
174
|
+
else
|
|
175
|
+
git -C "$ROOT" checkout -q --detach || true
|
|
176
|
+
fi
|
|
177
|
+
if on_branch "$ROOT" "$BRANCH"; then
|
|
178
|
+
echo "user-test-handoff: the main repo could not leave $BRANCH for $previous (an untracked file in the way?); move it and run restore again" >&2
|
|
179
|
+
exit 6
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
if ! on_branch "$WT" "$BRANCH"; then
|
|
183
|
+
git -C "$ROOT" worktree unlock "$WT" 2>/dev/null || true
|
|
184
|
+
git -C "$ROOT" worktree prune
|
|
185
|
+
if [ -e "$WT" ]; then
|
|
186
|
+
if ! { mkdir -p "$HOLD/leftover" && mv "$WT" "$HOLD/leftover/$(date +%s)"; }; then
|
|
187
|
+
echo "user-test-handoff: $WT exists and is not a worktree; move it and run restore again" >&2
|
|
188
|
+
exit 6
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
git -C "$ROOT" worktree add -q "$WT" "$BRANCH" || true
|
|
192
|
+
on_branch "$WT" "$BRANCH" || { echo "user-test-handoff: the worktree could not be re-added on $BRANCH" >&2; exit 6; }
|
|
193
|
+
fi
|
|
194
|
+
if [ -d "$HOLD/leftover" ]; then
|
|
195
|
+
mkdir -p "$COMMON/multi-agent-user-test-leftover" &&
|
|
196
|
+
mv "$HOLD/leftover" "$COMMON/multi-agent-user-test-leftover/$TASK-$(date +%s)" &&
|
|
197
|
+
echo "user-test-handoff: files found at the worktree path were kept under $COMMON/multi-agent-user-test-leftover" >&2
|
|
198
|
+
fi
|
|
199
|
+
return_entries
|
|
200
|
+
json restored "$previous"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
"$MODE"
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
// cat plan.json | node validate-planning.mjs - [--spec <path>]
|
|
13
13
|
//
|
|
14
14
|
// --spec names the analysis document or task text the plan was written from.
|
|
15
|
-
// A
|
|
16
|
-
//
|
|
15
|
+
// A named spec that cannot be read is an error, because the design checks
|
|
16
|
+
// below would otherwise be skipped without a trace. A plan more than
|
|
17
|
+
// SPEC_RATIO times its length gets a warning: a plan should record decisions,
|
|
18
|
+
// and one far longer than its spec is usually a transcript.
|
|
17
19
|
//
|
|
18
20
|
// Exit codes:
|
|
19
21
|
// 0 - valid
|
|
@@ -326,7 +328,7 @@ async function main() {
|
|
|
326
328
|
specText = readFileSync(spec, "utf-8");
|
|
327
329
|
specChars = specText.length;
|
|
328
330
|
} catch {
|
|
329
|
-
|
|
331
|
+
errors.push(`spec not readable: ${spec}`);
|
|
330
332
|
}
|
|
331
333
|
// With a design behind the plan, values come from its tokens, never from a
|
|
332
334
|
// number someone read off a screenshot.
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// 0 - valid, safe to act on
|
|
28
28
|
// 1 - invalid structure or required-field violation - caller MUST retry or fallback
|
|
29
29
|
// 2 - over-rejection guard tripped - caller MUST pause for human (or log+accept in autopilot)
|
|
30
|
+
// (takes precedence over 3; `corrected` is still set when a contradiction was fixed)
|
|
30
31
|
// 3 - contradiction auto-corrected - caller MAY proceed with corrected output
|
|
31
32
|
//
|
|
32
33
|
// Output: JSON result to stdout with shape:
|
|
@@ -427,7 +428,7 @@ function validate(raw) {
|
|
|
427
428
|
warnings.push(
|
|
428
429
|
`over-rejection guard: ${parsed.rejected.length}/${total} rejected (${(ratio * 100).toFixed(0)}%) - caller must pause for human confirm (triage=high-rejection-rate); autopilot may accept and log`,
|
|
429
430
|
);
|
|
430
|
-
triageCode =
|
|
431
|
+
triageCode = 2;
|
|
431
432
|
}
|
|
432
433
|
}
|
|
433
434
|
|
|
@@ -390,7 +390,8 @@ export function completionRefusal(next, payload, replace, diskStatus) {
|
|
|
390
390
|
* background launches approve nothing by design, and in analysis mode
|
|
391
391
|
* Phase 1 is not a plan, so neither is asked.
|
|
392
392
|
* - A `sessionId` already set is the run's identity for resume; a write that
|
|
393
|
-
* changes it detaches the run from the session that started it.
|
|
393
|
+
* changes it detaches the run from the session that started it. A replace
|
|
394
|
+
* document is the whole record, so leaving the field out also changes it.
|
|
394
395
|
* - Phase 2 to a later phase in an attended run with a tracked issue (jiraId
|
|
395
396
|
* or githubIssue) needs `phases["2"].trackerStatus` verified, declined or
|
|
396
397
|
* not-tracked: the issue moves to In Progress before any code, and a failed
|
|
@@ -399,15 +400,16 @@ export function completionRefusal(next, payload, replace, diskStatus) {
|
|
|
399
400
|
* @param {object} next the document as it would be written
|
|
400
401
|
* @param {unknown} payload the patch as sent
|
|
401
402
|
* @param {object} current the document on disk
|
|
403
|
+
* @param {{replace?: boolean}} [opts]
|
|
402
404
|
* @returns {string|null}
|
|
403
405
|
*/
|
|
404
|
-
export function transitionRefusal(next, payload, current) {
|
|
406
|
+
export function transitionRefusal(next, payload, current, { replace = false } = {}) {
|
|
405
407
|
const patch = payload && typeof payload === "object" ? payload : {};
|
|
406
408
|
const was = current && typeof current === "object" ? current : {};
|
|
407
409
|
if (
|
|
408
410
|
typeof was.sessionId === "string" &&
|
|
409
411
|
was.sessionId &&
|
|
410
|
-
Object.hasOwn(patch, "sessionId") &&
|
|
412
|
+
(replace || Object.hasOwn(patch, "sessionId")) &&
|
|
411
413
|
next.sessionId !== was.sessionId
|
|
412
414
|
) {
|
|
413
415
|
return `write-state: refusing to change sessionId from "${was.sessionId}". It identifies the run for resume; nothing was written.`;
|
|
@@ -531,6 +533,13 @@ async function main() {
|
|
|
531
533
|
let current = {};
|
|
532
534
|
if (replace) {
|
|
533
535
|
next = payload;
|
|
536
|
+
if (existsSync(path)) {
|
|
537
|
+
try {
|
|
538
|
+
current = JSON.parse(readFileSync(path, "utf-8"));
|
|
539
|
+
} catch {
|
|
540
|
+
current = {};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
534
543
|
} else {
|
|
535
544
|
if (existsSync(path)) {
|
|
536
545
|
try {
|
|
@@ -597,7 +606,7 @@ async function main() {
|
|
|
597
606
|
|
|
598
607
|
const refusal =
|
|
599
608
|
completionRefusal(next, payload, replace, diskStatus) ||
|
|
600
|
-
transitionRefusal(next, payload, current);
|
|
609
|
+
transitionRefusal(next, payload, current, { replace });
|
|
601
610
|
if (refusal) {
|
|
602
611
|
console.error(refusal);
|
|
603
612
|
releaseLock(path);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-31T20:07:26Z",
|
|
4
4
|
"skillCount": 238,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"path": "shared/core/multi-agent-manual-test/SKILL.md",
|
|
124
|
-
"sha256": "
|
|
124
|
+
"sha256": "8ea107eebf8160ec30306a18c619f10b3a4cf9ddbd741d37c52f09a534e2c687"
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
"path": "shared/core/multi-agent-model/SKILL.md",
|
|
@@ -20,12 +20,13 @@ Lets you switch to the task branch for manual testing in Xcode before the PR is
|
|
|
20
20
|
2. **Check the state** - the task must have completed Phase 2 (Dev) or later
|
|
21
21
|
- Phase < 2 → "No code has been written yet, run `resume #N` first"
|
|
22
22
|
|
|
23
|
-
3. **
|
|
23
|
+
3. **Hand the branch over**. Make the WIP commit yourself (Standard path item 3 of Phase 4 Step 2 folds it back), then let the script hold the run artefacts and the project root's current branch, remove the worktree and check the branch out. `{worktree-path}` and `{jiraId}` come from the task's state (`worktreePath`, `jiraId`):
|
|
24
24
|
```bash
|
|
25
|
-
|
|
26
|
-
git worktree
|
|
27
|
-
|
|
25
|
+
git -C "{worktree-path}" add -A
|
|
26
|
+
git -C "{worktree-path}" diff --cached --quiet || git -C "{worktree-path}" commit -m "WIP: {jiraId} - changes for user test"
|
|
27
|
+
bash $HOME/.claude/scripts/user-test-handoff.sh checkout --task "{jiraId}" --worktree "{worktree-path}" --root "{project-root}" --branch "{branch-name}"
|
|
28
28
|
```
|
|
29
|
+
A refused commit stops here with nothing moved. Script exits: 3 the project root has uncommitted changes; 5 the worktree or a submodule holds uncommitted, unpushed or stashed work (a submodule with no remote needs one added); both move nothing. 6 a handoff is already held, or git refused part way: run `restore` below (if it answers 7, nothing had moved). 1 the worktree is not on the branch.
|
|
29
30
|
|
|
30
31
|
4. **Give test instructions**:
|
|
31
32
|
```
|
|
@@ -41,7 +42,11 @@ Lets you switch to the task branch for manual testing in Xcode before the PR is
|
|
|
41
42
|
❌ "fix: ..." → the worktree is recreated and the fix is applied
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
5. **Wait for the user's response
|
|
45
|
+
5. **Wait for the user's response**, then hand the branch back before acting on it:
|
|
46
|
+
```bash
|
|
47
|
+
bash $HOME/.claude/scripts/user-test-handoff.sh restore --task "{jiraId}" --worktree "{worktree-path}" --root "{project-root}" --branch "{branch-name}"
|
|
48
|
+
```
|
|
49
|
+
Exit 3: the user edited files on the branch in the project root. Ask them to `git stash`, run `restore` again, then `git -C "{worktree-path}" stash pop`; a commit made there instead would stop Phase 4 from folding the checkpoints. Exit 6: the project root could not leave the branch (an untracked file in the way) or the worktree could not be re-added; the message names the cause, the hold is kept, and `restore` runs again once it is cleared. Exit 7: nothing was held, so the worktree was never removed.
|
|
45
50
|
|
|
46
51
|
6. **Based on the result**:
|
|
47
52
|
- **OK** → an "ok" is a claim, and the pass has to be substantiated before it is recorded. First write `$WORKTREE/.pipeline/manual-test.json`, one entry per acceptance criterion taken from the analysis doc test plan, the plan tasks, or the user's own words:
|
|
@@ -53,5 +58,5 @@ Lets you switch to the task branch for manual testing in Xcode before the PR is
|
|
|
53
58
|
node $HOME/.claude/scripts/evidence-gate.mjs --claim manual --status passed \
|
|
54
59
|
--evidence "$WORKTREE/.pipeline/manual-test.json"
|
|
55
60
|
```
|
|
56
|
-
Exit 1 means the "ok" is not accepted: name the criterion that lacks evidence and wait for the next reply. Exit 0 →
|
|
57
|
-
- **Fix needed** →
|
|
61
|
+
Exit 1 means the "ok" is not accepted: name the criterion that lacks evidence and wait for the next reply. Exit 0 → proceed to Phase 4. Full contract: `$HOME/.claude/multi-agent-refs/phases/phase-3-review.md` step 5.
|
|
62
|
+
- **Fix needed** → apply the fix in the restored worktree
|