duaer-spec 0.15.0 → 0.16.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/.duaer/active-job.json +4 -4
- package/.duaer/memory/testing.md +7 -2
- package/.duaer/specs/056-revise-reliability/delivery.json +13 -0
- package/.duaer/specs/056-revise-reliability/spec.md +44 -0
- package/.duaer/specs/056-revise-reliability/tasks.md +10 -0
- package/.duaer/specs/057-progress-accepted/delivery.json +9 -0
- package/.duaer/specs/057-progress-accepted/spec.md +42 -0
- package/.duaer/specs/057-progress-accepted/tasks.md +8 -0
- package/.duaer/specs/058-revise-progress-track/delivery.json +13 -0
- package/.duaer/specs/058-revise-progress-track/spec.md +48 -0
- package/.duaer/specs/058-revise-progress-track/tasks.md +9 -0
- package/.duaer/specs/059-revise-preempt-order/delivery.json +13 -0
- package/.duaer/specs/059-revise-preempt-order/spec.md +47 -0
- package/.duaer/specs/059-revise-preempt-order/tasks.md +8 -0
- package/.duaer/specs/060-release-016/delivery.json +10 -0
- package/.duaer/specs/060-release-016/spec.md +24 -0
- package/.duaer/specs/060-release-016/tasks.md +4 -0
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/duaer-live.mjs +365 -53
- package/docs/agent/e2e-test-plan.md +7 -0
- package/package.json +1 -1
- package/web/live-dev/app.js +74 -15
- package/web/live-dev/i18n.js +33 -0
package/.duaer/active-job.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"featureDir": ".duaer/specs/
|
|
3
|
-
"branch": "feat/release-
|
|
4
|
-
"worktree": ".worktree/feat-release-
|
|
5
|
-
"status": "
|
|
2
|
+
"featureDir": ".duaer/specs/060-release-016",
|
|
3
|
+
"branch": "feat/release-016",
|
|
4
|
+
"worktree": ".worktree/feat-release-016",
|
|
5
|
+
"status": "accepted"
|
|
6
6
|
}
|
package/.duaer/memory/testing.md
CHANGED
|
@@ -41,8 +41,13 @@ Covers without a paid LLM:
|
|
|
41
41
|
3. `/api/validate` pass and `/api/validate/fix` via an in-process mock
|
|
42
42
|
OpenAI-compatible server
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
Also run `npm test` (includes revise-reliability contract tests: Brief
|
|
45
|
+
snapshot/rollback helpers + source markers for `PREEMPT_FAILED` / longer
|
|
46
|
+
client timeout).
|
|
47
|
+
|
|
48
|
+
Does **not** cover: real model quality, live Terminal CLI launch against a
|
|
49
|
+
real Cursor/Claude session. Those stay manual E2E catalog rows (E2E-055) or
|
|
50
|
+
future suites.
|
|
46
51
|
|
|
47
52
|
## Opt-in (still need an explicit ask or Spec note)
|
|
48
53
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"summary": "Revise dispatch: honest preempt, Brief rollback on launch fail, structured retryable errors, terminal status line, longer client wait.",
|
|
4
|
+
"verification": {
|
|
5
|
+
"levels": ["L0", "L3"],
|
|
6
|
+
"commands": [
|
|
7
|
+
"node --check bin/duaer-live.mjs",
|
|
8
|
+
"npm run test:live",
|
|
9
|
+
"node --test test/revise-reliability.test.mjs"
|
|
10
|
+
],
|
|
11
|
+
"notes": "E2E-055 manual for live Terminal preempt path."
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Feature Specification: Revise round reliability
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `feat/revise-reliability`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-17
|
|
6
|
+
|
|
7
|
+
**Status**: Active
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Revise dispatch is **observable**, **retryable**, and failures are **readable**:
|
|
12
|
+
confirm → Terminal enqueue must not report success when preempt failed; Brief
|
|
13
|
+
mutations must not stick after a failed launch; client timeout matches server
|
|
14
|
+
budgets; status exposes Terminal busy/queue; UI keeps dispatch unlocked when
|
|
15
|
+
retryable.
|
|
16
|
+
|
|
17
|
+
## In scope
|
|
18
|
+
|
|
19
|
+
- Honest preempt outcome (no false preempted / false HTTP 200)
|
|
20
|
+
- Rollback Brief writes if launch/enqueue fails
|
|
21
|
+
- Structured revise errors (`code`, `retryable`) + UI copy
|
|
22
|
+
- Align client `/api/revise` abort with recreate/init budgets (~3 min)
|
|
23
|
+
- `/api/status` includes `terminal: { busy, queueDepth, runnerHealthy }`
|
|
24
|
+
- Minimal status/hint line; extend L3 smoke for error shape / rollback unit
|
|
25
|
+
|
|
26
|
+
## Out of scope
|
|
27
|
+
|
|
28
|
+
- Desk layout / column redesign
|
|
29
|
+
- Full Playwright Terminal automation
|
|
30
|
+
- Paid live LLM quality
|
|
31
|
+
|
|
32
|
+
## Acceptance
|
|
33
|
+
|
|
34
|
+
1. When preempt cannot clear leftover agents, `/api/revise` fails with
|
|
35
|
+
`PREEMPT_FAILED` and does **not** lock the revise card
|
|
36
|
+
2. Failed launch does not leave a new Revision block in Brief (rollback)
|
|
37
|
+
3. Client wait ≥ worst-case recreate path; timeout message mentions retry
|
|
38
|
+
4. Status poll shows terminal busy/queue when revising
|
|
39
|
+
5. `npm run test:live` still passes; new smoke/unit covers rollback or error codes
|
|
40
|
+
|
|
41
|
+
## Assumptions
|
|
42
|
+
|
|
43
|
+
- Worktree: `.worktree/feat-revise-reliability`
|
|
44
|
+
- Brief: `.duaer/specs/056-revise-reliability/`
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Tasks: Revise round reliability
|
|
2
|
+
|
|
3
|
+
**Brief**: `.duaer/specs/056-revise-reliability/`
|
|
4
|
+
|
|
5
|
+
## Checklist
|
|
6
|
+
|
|
7
|
+
- [x] Honest preempt + PREEMPT_FAILED; Brief rollback on launch fail
|
|
8
|
+
- [x] Structured errors on `/api/revise`; status.terminal
|
|
9
|
+
- [x] UI: longer timeout, retryable unlock, terminal line, i18n
|
|
10
|
+
- [x] L3 / unit smoke; docs; accept; merge develop
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Feature Specification: Progress sync when delivery accepted
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `fix/progress-accepted`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-17
|
|
6
|
+
|
|
7
|
+
**Status**: Active
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Task progress must not show `0/N · delivery accepted · 工单完成`. When
|
|
12
|
+
`delivery.json` is `accepted`, the desk treats tasks as complete (done ===
|
|
13
|
+
total) and reconciles unchecked boxes in `tasks.md` when safe.
|
|
14
|
+
|
|
15
|
+
## Problem
|
|
16
|
+
|
|
17
|
+
`dispatchStatus` only rewrites `progress.current` on accept; `done` stays at
|
|
18
|
+
the unchecked count. Agents often stamp `delivery.json` without flipping every
|
|
19
|
+
`- [ ]` to `- [x]`, so 任务跟进 shows a contradictory `0/3 · …工单完成`.
|
|
20
|
+
|
|
21
|
+
## In scope
|
|
22
|
+
|
|
23
|
+
- Normalize progress when accepted: `done = total`, all task rows `done: true`
|
|
24
|
+
- Reconcile `tasks.md` unchecked boxes to `[x]` once when accepted (idempotent)
|
|
25
|
+
- Unit/smoke coverage; short README/CHANGELOG/E2E note
|
|
26
|
+
|
|
27
|
+
## Out of scope
|
|
28
|
+
|
|
29
|
+
- Changing how agents are prompted (except if a one-line reminder helps)
|
|
30
|
+
- Progress column layout redesign
|
|
31
|
+
|
|
32
|
+
## Acceptance
|
|
33
|
+
|
|
34
|
+
1. Accepted delivery never surfaces `done < total` with an “工单完成 /
|
|
35
|
+
delivery accepted” current line
|
|
36
|
+
2. Unchecked tasks in Brief are marked `[x]` after status observes accept
|
|
37
|
+
3. `npm test` / `npm run test:live` pass
|
|
38
|
+
|
|
39
|
+
## Assumptions
|
|
40
|
+
|
|
41
|
+
- Worktree: `.worktree/fix-progress-accepted`
|
|
42
|
+
- Brief: `.duaer/specs/057-progress-accepted/`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"acceptedAt": "2026-09-17T09:50:00.000Z",
|
|
4
|
+
"summary": "Revise-round progress scopes to Revision N / R{n}-* tasks; lagging accepted + busy Terminal no longer freezes on 工单完成.",
|
|
5
|
+
"verification": {
|
|
6
|
+
"levels": ["L0", "L1", "L3"],
|
|
7
|
+
"commands": [
|
|
8
|
+
"node --check bin/duaer-live.mjs",
|
|
9
|
+
"npm test"
|
|
10
|
+
],
|
|
11
|
+
"result": "pass (14 tests)"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Feature Specification: Track revise-round task progress
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `fix/revise-progress-track`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-17
|
|
6
|
+
|
|
7
|
+
**Status**: Accepted
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
After **Confirm revise and dispatch**, 任务跟进 shows **this revision’s**
|
|
12
|
+
checklist and updates as work starts — not the prior dispatch’s completed
|
|
13
|
+
`T001…` pile, and not a frozen “delivery accepted · 工单完成” while Terminal
|
|
14
|
+
is already running the revise.
|
|
15
|
+
|
|
16
|
+
## Problem
|
|
17
|
+
|
|
18
|
+
Progress parses the whole `tasks.md`. After revise, old tasks stay `[x]` so
|
|
19
|
+
the bar looks finished (or accepted-normalized to N/N) while the new `R{n}-*`
|
|
20
|
+
round has started. Live `revisionCount` can lag; status may still say
|
|
21
|
+
`accepted`, so the wrong run-block is updated and revise work looks untracked.
|
|
22
|
+
|
|
23
|
+
## In scope
|
|
24
|
+
|
|
25
|
+
- Scope progress to dispatch (pre-Revision section) vs active `Revision N`
|
|
26
|
+
- Infer active revision from job / delivery / tasks.md headers and `R{n}` ids
|
|
27
|
+
- While revising (or Terminal busy with open `R{n}` tasks), do not force
|
|
28
|
+
accept-complete progress
|
|
29
|
+
- Persist inferred `revisionCount` onto the live job when missing
|
|
30
|
+
- Tests + CHANGELOG / E2E note
|
|
31
|
+
|
|
32
|
+
## Out of scope
|
|
33
|
+
|
|
34
|
+
- Layout / column redesign
|
|
35
|
+
- Changing revise launch / preempt behavior
|
|
36
|
+
|
|
37
|
+
## Acceptance
|
|
38
|
+
|
|
39
|
+
1. After Confirm revise, progress shows only this revision’s tasks (e.g. `0/3`
|
|
40
|
+
then rising), under the Revision N run block
|
|
41
|
+
2. While revise is active, status is not stuck on `delivery accepted · 工单完成`
|
|
42
|
+
3. When revise delivery accepts, that revision shows `N/N · …完成`
|
|
43
|
+
4. `npm test` passes
|
|
44
|
+
|
|
45
|
+
## Assumptions
|
|
46
|
+
|
|
47
|
+
- Worktree: `.worktree/fix-revise-progress-track`
|
|
48
|
+
- Brief: `.duaer/specs/058-revise-progress-track/`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Tasks: Track revise-round task progress
|
|
2
|
+
|
|
3
|
+
**Brief**: `.duaer/specs/058-revise-progress-track/`
|
|
4
|
+
|
|
5
|
+
## Checklist
|
|
6
|
+
|
|
7
|
+
- [x] Scope parseTasksProgress by revision; infer active revision
|
|
8
|
+
- [x] Revising / busy Terminal skips accept-complete normalize
|
|
9
|
+
- [x] Tests + docs; merge develop
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"acceptedAt": "2026-09-17T10:05:00.000Z",
|
|
4
|
+
"summary": "priorAccepted revise preempts leftovers before enqueue so Confirm revise no longer returns PREEMPT_FAILED while the revise Agent is already running.",
|
|
5
|
+
"verification": {
|
|
6
|
+
"levels": ["L0", "L1", "L3"],
|
|
7
|
+
"commands": [
|
|
8
|
+
"node --check bin/duaer-live.mjs",
|
|
9
|
+
"npm test"
|
|
10
|
+
],
|
|
11
|
+
"result": "pass (14 tests)"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Feature Specification: Honest revise preempt (enqueue after clear)
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `fix/revise-preempt-order`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-17
|
|
6
|
+
|
|
7
|
+
**Status**: Accepted
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
After Confirm revise, either the revise Agent is launched and the desk
|
|
12
|
+
reports success, or preempt truly failed and nothing was started — never
|
|
13
|
+
“Could not preempt…” while Terminal is already running the revise job.
|
|
14
|
+
|
|
15
|
+
## Problem
|
|
16
|
+
|
|
17
|
+
`launchInTerminal` enqueues the revise job **before** preempt. Within the
|
|
18
|
+
post-preempt wait, the runner starts the new Agent; status then sees
|
|
19
|
+
`busy && agentsLeft > 0` and throws `PREEMPT_FAILED`. The API rolls back
|
|
20
|
+
Brief while the queued revise Agent keeps working — UI error, work already
|
|
21
|
+
underway; retries kill that Agent.
|
|
22
|
+
|
|
23
|
+
## In scope
|
|
24
|
+
|
|
25
|
+
- Preempt leftover agents **before** enqueueing the revise Terminal job
|
|
26
|
+
- Fail `PREEMPT_FAILED` only when prior agents remain (nothing queued yet)
|
|
27
|
+
- Do not treat the newly started revise Agent as a preempt failure
|
|
28
|
+
- Tests + CHANGELOG / E2E note
|
|
29
|
+
|
|
30
|
+
## Out of scope
|
|
31
|
+
|
|
32
|
+
- Progress scoping (058)
|
|
33
|
+
- Changing when priorAccepted revise preempts vs waits
|
|
34
|
+
|
|
35
|
+
## Acceptance
|
|
36
|
+
|
|
37
|
+
1. priorAccepted revise: preempt clears leftovers, then enqueue; HTTP 200
|
|
38
|
+
2. If leftover agents cannot be cleared, `PREEMPT_FAILED` and **no** new
|
|
39
|
+
Terminal job for that attempt
|
|
40
|
+
3. Desk does not show preempt failure while revise Agent is already running
|
|
41
|
+
from that same Confirm tap
|
|
42
|
+
4. `npm test` passes
|
|
43
|
+
|
|
44
|
+
## Assumptions
|
|
45
|
+
|
|
46
|
+
- Worktree: `.worktree/fix-revise-preempt-order`
|
|
47
|
+
- Brief: `.duaer/specs/059-revise-preempt-order/`
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "accepted",
|
|
3
|
+
"acceptedAt": "2026-09-18T00:00:00.000Z",
|
|
4
|
+
"summary": "Release 0.16.0: honest revise preempt, revise progress scope, accept progress sync.",
|
|
5
|
+
"verification": {
|
|
6
|
+
"levels": ["L0", "L1", "L3"],
|
|
7
|
+
"commands": ["npm test"],
|
|
8
|
+
"result": "pass (14 tests)"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Feature Specification: Release 0.16.0
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `feat/release-016`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-18
|
|
6
|
+
|
|
7
|
+
**Status**: Accepted
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Ship **0.16.0** to `main` + npm: revise reliability (honest preempt order),
|
|
12
|
+
revise-round progress tracking, and accept progress sync.
|
|
13
|
+
|
|
14
|
+
## Acceptance
|
|
15
|
+
|
|
16
|
+
1. `package.json` version `0.16.0`; CHANGELOG dated section; Unreleased cleared
|
|
17
|
+
2. `main` tagged `v0.16.0`; GitHub Release; npm `latest` is `0.16.0`
|
|
18
|
+
3. `npm test` passes before promote
|
|
19
|
+
|
|
20
|
+
## Assumptions
|
|
21
|
+
|
|
22
|
+
- Worktree: `.worktree/feat-release-016`
|
|
23
|
+
- Brief: `.duaer/specs/060-release-016/`
|
|
24
|
+
- User ask「发版」authorizes push + promote `develop` → `main`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.16.0 — 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Honest revise preempt
|
|
8
|
+
|
|
9
|
+
- priorAccepted revise preempts leftover Agents **before** enqueueing the new
|
|
10
|
+
Terminal job (no false `PREEMPT_FAILED` while the revise Agent already runs)
|
|
11
|
+
- `PREEMPT_FAILED` only when leftovers remain — nothing queued for that attempt
|
|
12
|
+
|
|
13
|
+
### Revise progress tracking
|
|
14
|
+
|
|
15
|
+
- After Confirm revise, task progress scopes to the active `Revision N` checklist
|
|
16
|
+
(not the prior dispatch’s completed `T*` pile)
|
|
17
|
+
- While revise is active (or Terminal is busy on open `R{n}` work), status does
|
|
18
|
+
not freeze on the accepted “job complete” label
|
|
19
|
+
- Live `revisionCount` is inferred from Brief/`tasks.md` when the job stamp lags
|
|
20
|
+
|
|
21
|
+
### Progress sync when delivery accepted
|
|
22
|
+
|
|
23
|
+
- When `delivery.json` is `accepted`, task progress shows `N/N` (not `0/N` with
|
|
24
|
+
an accepted label)
|
|
25
|
+
- Unchecked `tasks.md` boxes are reconciled to `[x]` on status poll after accept
|
|
26
|
+
|
|
27
|
+
### Revise round reliability
|
|
28
|
+
|
|
29
|
+
- Honest Terminal preempt: no false `preempted` / HTTP 200 when agents still run (`PREEMPT_FAILED`, retryable)
|
|
30
|
+
- Rollback Brief `spec.md` / `tasks.md` / `delivery.json` if revise launch fails
|
|
31
|
+
- `/api/status` includes `terminal: { busy, queueDepth, runnerHealthy }`; desk shows the line
|
|
32
|
+
- Client revise wait raised to ~3 minutes; structured error codes + keep Confirm revise unlocked on failure
|
|
33
|
+
- Stuck revising (busy Terminal, no task progress) resurfaces **Revise again**
|
|
34
|
+
|
|
3
35
|
## 0.15.0 — 2026-09-17
|
|
4
36
|
|
|
5
37
|
### Live desk L3 smoke
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ mock LLM — no paid API).
|
|
|
77
77
|
3. Choose a **CLI** digital employee: **Cursor Agent** or **Claude Code** (install Cursor CLI if needed: `curl https://cursor.com/install -fsS | bash`)
|
|
78
78
|
4. Edit the start command (must begin with `Duaer`) → dispatch creates `.worktree/feat-*` and opens **Terminal** to run the CLI
|
|
79
79
|
5. Watch progress in the right column (`tasks.md`); when `delivery.json` is `accepted`, open **View product** (`preview.url` or auto-detected `index.html`)
|
|
80
|
-
6. If the result is not right: click **Continue improving (left chat)**, clarify why and what to change, confirm the revise card (same validate gate), then the desk relaunches
|
|
80
|
+
6. If the result is not right: click **Continue improving (left chat)**, clarify why and what to change, confirm the revise card (same validate gate), then the desk relaunches on the same worktree (preempt leftover Agents before enqueueing revise). Progress tracks this revision’s `R{n}-*` tasks. Failed launch rolls back the Brief Revision and keeps **Confirm revise** available; status shows Terminal busy/queue.
|
|
81
81
|
7. If the job needs hosting / a public URL, deploy with **GitHub CLI (`gh`) + Actions** (Pages template under `.duaer/templates/deploy-github-pages.yml`; see [`docs/agent/deploy-github.md`](docs/agent/deploy-github.md))
|
|
82
82
|
|
|
83
83
|
Worktree id ≠ Brief folder name — use `.worktree/feat-<name>/`, Brief under `.duaer/specs/<nnn-slug>/` inside that worktree.
|
package/README.zh-CN.md
CHANGED
|
@@ -75,7 +75,7 @@ duaer live
|
|
|
75
75
|
3. 只选 **CLI 数字员工**:**Cursor Agent** 或 **Claude Code**(未装 Cursor 时页面会提示:`curl https://cursor.com/install -fsS | bash`)
|
|
76
76
|
4. 编辑「启动命令」(须以 `Duaer` 开头)→ 派工建 `.worktree/feat-*`,并打开 **Terminal** 跑对应 CLI
|
|
77
77
|
5. 右侧进度列轮询 `tasks.md`;`delivery.json` 为 `accepted` 后出现 **查看成品**(`preview.url`,或自动发现 `index.html`)
|
|
78
|
-
6. 成品不满意:点
|
|
78
|
+
6. 成品不满意:点 **继续改进(左侧对话)**,说清原因与期望,确认改进卡(同样须校验通过)后续派(先清残留 Agent 再入队)。进度跟本轮 `R{n}-*` 任务。启动失败会回滚本轮 Brief Revision,并可再点「再派一版」;状态行显示 Terminal 忙闲/队列。
|
|
79
79
|
7. 若需求需要上线 / 公网访问:默认用 **GitHub CLI(`gh`)+ Actions** 自动化部署(静态站可参考 `.duaer/templates/deploy-github-pages.yml`;说明见英文 [`docs/agent/deploy-github.md`](docs/agent/deploy-github.md))
|
|
80
80
|
|
|
81
81
|
### 文档语言
|
package/bin/duaer-live.mjs
CHANGED
|
@@ -1908,12 +1908,51 @@ function preemptBusyTerminalJob(queueDir, { worktreePath = null, logPath = null
|
|
|
1908
1908
|
}
|
|
1909
1909
|
return {
|
|
1910
1910
|
preempted: killed.length > 0,
|
|
1911
|
-
reason: killed.join(",")
|
|
1911
|
+
reason: killed.length > 0 ? killed.join(",") : "signal-attempted",
|
|
1912
1912
|
killed,
|
|
1913
1913
|
agentPids: agentTargets.map((t) => t.pid),
|
|
1914
1914
|
};
|
|
1915
1915
|
}
|
|
1916
1916
|
|
|
1917
|
+
class LaunchGateError extends Error {
|
|
1918
|
+
constructor(message, { code = "LAUNCH_FAILED", retryable = true, detail = null } = {}) {
|
|
1919
|
+
super(message);
|
|
1920
|
+
this.name = "LaunchGateError";
|
|
1921
|
+
this.code = code;
|
|
1922
|
+
this.retryable = Boolean(retryable);
|
|
1923
|
+
this.detail = detail;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function snapshotTextFile(filePath) {
|
|
1928
|
+
if (!fs.existsSync(filePath)) return { exists: false, content: "" };
|
|
1929
|
+
return { exists: true, content: fs.readFileSync(filePath, "utf8") };
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function restoreTextFile(filePath, snap) {
|
|
1933
|
+
if (!snap?.exists) {
|
|
1934
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
fs.writeFileSync(filePath, snap.content, "utf8");
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function terminalQueueSnapshot(worktreePath) {
|
|
1941
|
+
if (!worktreePath || !fs.existsSync(worktreePath)) {
|
|
1942
|
+
return {
|
|
1943
|
+
busy: false,
|
|
1944
|
+
queueDepth: 0,
|
|
1945
|
+
runnerHealthy: false,
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
const qdir = terminalQueueDir(worktreePath);
|
|
1949
|
+
return {
|
|
1950
|
+
busy: isTerminalRunnerBusy(qdir),
|
|
1951
|
+
queueDepth: countQueuedJobs(qdir),
|
|
1952
|
+
runnerHealthy: isTerminalRunnerHealthy(qdir),
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1917
1956
|
function terminalJobsDir(queueDir) {
|
|
1918
1957
|
return path.join(queueDir, "jobs");
|
|
1919
1958
|
}
|
|
@@ -1978,30 +2017,65 @@ function launchInTerminal({
|
|
|
1978
2017
|
if (key && fs.existsSync(key)) {
|
|
1979
2018
|
const qdir = terminalQueueDir(key);
|
|
1980
2019
|
fs.mkdirSync(qdir, { recursive: true });
|
|
1981
|
-
const jobPath = enqueueTerminalJob(qdir, commandLine);
|
|
1982
|
-
let queuedCount = countQueuedJobs(qdir);
|
|
1983
2020
|
|
|
1984
2021
|
if (isTerminalRunnerHealthy(qdir)) {
|
|
1985
2022
|
let busy = isTerminalRunnerBusy(qdir);
|
|
1986
2023
|
let preempted = false;
|
|
2024
|
+
let pre = null;
|
|
1987
2025
|
// After accept, leftover first-agent must not block revise forever.
|
|
2026
|
+
// Preempt *before* enqueue so the new revise Agent is never mistaken
|
|
2027
|
+
// for a leftover (false PREEMPT_FAILED while work already started).
|
|
1988
2028
|
if (busy && preemptBusy) {
|
|
1989
|
-
|
|
2029
|
+
pre = preemptBusyTerminalJob(qdir, {
|
|
1990
2030
|
worktreePath: cwd,
|
|
1991
2031
|
logPath,
|
|
1992
2032
|
});
|
|
1993
|
-
preempted =
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2033
|
+
preempted = Array.isArray(pre.killed) && pre.killed.length > 0;
|
|
2034
|
+
let agentsLeft = cwd ? listWorktreeAgentPids(cwd).length : 0;
|
|
2035
|
+
for (let i = 0; i < 10 && agentsLeft > 0; i += 1) {
|
|
2036
|
+
sleepBriefMs(200);
|
|
2037
|
+
if (i === 4 || i === 8) {
|
|
2038
|
+
preemptBusyTerminalJob(qdir, {
|
|
2039
|
+
worktreePath: cwd,
|
|
2040
|
+
logPath,
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
agentsLeft = cwd ? listWorktreeAgentPids(cwd).length : 0;
|
|
2044
|
+
}
|
|
2045
|
+
if (agentsLeft > 0) {
|
|
2046
|
+
throw new LaunchGateError(
|
|
2047
|
+
"无法抢占仍在运行的 Agent。请结束该 worktree 的 Terminal 任务后重试续派。",
|
|
2048
|
+
{
|
|
2049
|
+
code: "PREEMPT_FAILED",
|
|
2050
|
+
retryable: true,
|
|
2051
|
+
detail: {
|
|
2052
|
+
busy: isTerminalRunnerBusy(qdir),
|
|
2053
|
+
agentsLeft,
|
|
2054
|
+
killed: pre?.killed || [],
|
|
2055
|
+
reason: pre?.reason || null,
|
|
2056
|
+
enqueued: false,
|
|
2057
|
+
},
|
|
2058
|
+
},
|
|
2000
2059
|
);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2060
|
+
}
|
|
2061
|
+
// Best-effort: let runner drop running.cmd before we queue revise.
|
|
2062
|
+
for (let i = 0; i < 10; i += 1) {
|
|
2063
|
+
if (!isTerminalRunnerBusy(qdir)) break;
|
|
2064
|
+
sleepBriefMs(150);
|
|
2003
2065
|
}
|
|
2004
2066
|
}
|
|
2067
|
+
|
|
2068
|
+
const jobPath = enqueueTerminalJob(qdir, commandLine);
|
|
2069
|
+
const queuedCount = countQueuedJobs(qdir);
|
|
2070
|
+
busy = isTerminalRunnerBusy(qdir);
|
|
2071
|
+
try {
|
|
2072
|
+
fs.writeFileSync(
|
|
2073
|
+
path.join(qdir, "wake"),
|
|
2074
|
+
`${path.basename(jobPath)}\n`,
|
|
2075
|
+
);
|
|
2076
|
+
} catch {
|
|
2077
|
+
// ignore
|
|
2078
|
+
}
|
|
2005
2079
|
const pid = readRunnerPid(qdir);
|
|
2006
2080
|
appendLaunchLog(
|
|
2007
2081
|
logPath,
|
|
@@ -2021,6 +2095,7 @@ function launchInTerminal({
|
|
|
2021
2095
|
}
|
|
2022
2096
|
|
|
2023
2097
|
// Stale/dead PID or pre-heartbeat runner: do not claim reuse
|
|
2098
|
+
const jobPath = enqueueTerminalJob(qdir, commandLine);
|
|
2024
2099
|
const stalePid = readRunnerPid(qdir);
|
|
2025
2100
|
if (stalePid != null) {
|
|
2026
2101
|
appendLaunchLog(
|
|
@@ -3022,10 +3097,11 @@ async function reviseDispatchedJob({
|
|
|
3022
3097
|
const specPath = path.join(dispatch.featureDir, "spec.md");
|
|
3023
3098
|
const tasksPath = path.join(dispatch.featureDir, "tasks.md");
|
|
3024
3099
|
const deliveryPath = path.join(dispatch.featureDir, "delivery.json");
|
|
3100
|
+
const snapSpec = snapshotTextFile(specPath);
|
|
3101
|
+
const snapTasks = snapshotTextFile(tasksPath);
|
|
3102
|
+
const snapDelivery = snapshotTextFile(deliveryPath);
|
|
3025
3103
|
|
|
3026
|
-
let specMd =
|
|
3027
|
-
? fs.readFileSync(specPath, "utf8")
|
|
3028
|
-
: "";
|
|
3104
|
+
let specMd = snapSpec.exists ? snapSpec.content : "";
|
|
3029
3105
|
specMd = specMd.replace(/\*\*Status\*\*:\s*.+$/m, `**Status**: Revising (r${revN})`);
|
|
3030
3106
|
if (!/\*\*Status\*\*:/.test(specMd)) {
|
|
3031
3107
|
specMd = `${specMd.trim()}\n\n**Status**: Revising (r${revN})\n`;
|
|
@@ -3047,9 +3123,7 @@ ${reasonLine || text}
|
|
|
3047
3123
|
`;
|
|
3048
3124
|
fs.writeFileSync(specPath, `${specMd.trim()}\n${revisionBlock}\n`, "utf8");
|
|
3049
3125
|
|
|
3050
|
-
let tasksMd =
|
|
3051
|
-
? fs.readFileSync(tasksPath, "utf8")
|
|
3052
|
-
: "# Tasks\n\n";
|
|
3126
|
+
let tasksMd = snapTasks.exists ? snapTasks.content : "# Tasks\n\n";
|
|
3053
3127
|
const taskBlock = `
|
|
3054
3128
|
- [ ] R${revN}-1 Apply revision: ${restated.change.replace(/\n/g, " ").slice(0, 160)}
|
|
3055
3129
|
- [ ] R${revN}-2 Verify against revision acceptance
|
|
@@ -3062,9 +3136,9 @@ ${reasonLine || text}
|
|
|
3062
3136
|
);
|
|
3063
3137
|
|
|
3064
3138
|
let prevDelivery = null;
|
|
3065
|
-
if (
|
|
3139
|
+
if (snapDelivery.exists) {
|
|
3066
3140
|
try {
|
|
3067
|
-
prevDelivery = JSON.parse(
|
|
3141
|
+
prevDelivery = JSON.parse(snapDelivery.content);
|
|
3068
3142
|
} catch {
|
|
3069
3143
|
prevDelivery = null;
|
|
3070
3144
|
}
|
|
@@ -3093,11 +3167,23 @@ ${reasonLine || text}
|
|
|
3093
3167
|
chosen = detected.agents[0]?.id || "";
|
|
3094
3168
|
}
|
|
3095
3169
|
if (!chosen) {
|
|
3096
|
-
|
|
3170
|
+
restoreTextFile(specPath, snapSpec);
|
|
3171
|
+
restoreTextFile(tasksPath, snapTasks);
|
|
3172
|
+
restoreTextFile(deliveryPath, snapDelivery);
|
|
3173
|
+
throw new LaunchGateError("未检测到可用 CLI(Cursor Agent / Claude Code)", {
|
|
3174
|
+
code: "NO_AGENT",
|
|
3175
|
+
retryable: true,
|
|
3176
|
+
});
|
|
3097
3177
|
}
|
|
3098
3178
|
const ok = detected.agents.some((a) => a.id === chosen && a.available);
|
|
3099
3179
|
if (!ok) {
|
|
3100
|
-
|
|
3180
|
+
restoreTextFile(specPath, snapSpec);
|
|
3181
|
+
restoreTextFile(tasksPath, snapTasks);
|
|
3182
|
+
restoreTextFile(deliveryPath, snapDelivery);
|
|
3183
|
+
throw new LaunchGateError(`未安装启动器:${chosen}`, {
|
|
3184
|
+
code: "AGENT_MISSING",
|
|
3185
|
+
retryable: true,
|
|
3186
|
+
});
|
|
3101
3187
|
}
|
|
3102
3188
|
|
|
3103
3189
|
// After accepted delivery (or recreated worktree), do not use --continue:
|
|
@@ -3153,17 +3239,37 @@ ${restated.keep}
|
|
|
3153
3239
|
logPath,
|
|
3154
3240
|
`\n—— revise r${revN} ${now} (continue=${continueSession} recreated=${recreated} priorAccepted=${priorAccepted}) ——\n${restated.summary}\n`,
|
|
3155
3241
|
);
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3242
|
+
|
|
3243
|
+
let launch;
|
|
3244
|
+
try {
|
|
3245
|
+
launch = launchAgent({
|
|
3246
|
+
agentId: chosen,
|
|
3247
|
+
worktreePath: dispatch.worktreePath,
|
|
3248
|
+
agentPrompt,
|
|
3249
|
+
logPath,
|
|
3250
|
+
featureDir: dispatch.featureDir,
|
|
3251
|
+
continueSession,
|
|
3252
|
+
// Revise after accept/recreate must not wait forever behind leftover agent.
|
|
3253
|
+
reviseLaunch: true,
|
|
3254
|
+
// preempt only when !continueSession (priorAccepted || recreated) — see launchAgent
|
|
3255
|
+
});
|
|
3256
|
+
} catch (err) {
|
|
3257
|
+
restoreTextFile(specPath, snapSpec);
|
|
3258
|
+
restoreTextFile(tasksPath, snapTasks);
|
|
3259
|
+
restoreTextFile(deliveryPath, snapDelivery);
|
|
3260
|
+
if (err?.code === "PREEMPT_FAILED" || err?.name === "LaunchGateError") {
|
|
3261
|
+
throw err;
|
|
3262
|
+
}
|
|
3263
|
+
const wrapped = new LaunchGateError(
|
|
3264
|
+
err instanceof Error ? err.message : "续派启动失败",
|
|
3265
|
+
{
|
|
3266
|
+
code: err?.code === "CHILD_TIMEOUT" ? "CHILD_TIMEOUT" : "LAUNCH_FAILED",
|
|
3267
|
+
retryable: true,
|
|
3268
|
+
detail: { cause: err?.code || err?.name || null },
|
|
3269
|
+
},
|
|
3270
|
+
);
|
|
3271
|
+
throw wrapped;
|
|
3272
|
+
}
|
|
3167
3273
|
rememberPreferredAgent(chosen);
|
|
3168
3274
|
|
|
3169
3275
|
const nextDispatch = {
|
|
@@ -3214,15 +3320,53 @@ function extractSection(md, title) {
|
|
|
3214
3320
|
return m ? m[1].trim() : "";
|
|
3215
3321
|
}
|
|
3216
3322
|
|
|
3217
|
-
function
|
|
3323
|
+
function inferRevisionFromTasksMd(tasksMd) {
|
|
3324
|
+
let max = 0;
|
|
3325
|
+
for (const line of String(tasksMd || "").split(/\r?\n/)) {
|
|
3326
|
+
const header = line.match(/^##\s+Revision\s+(\d+)\b/i);
|
|
3327
|
+
if (header) max = Math.max(max, Number(header[1]) || 0);
|
|
3328
|
+
const item = line.match(/^\s*[-*]\s+\[[ xX]\]\s+R(\d+)(?:[-.\s]|$)/i);
|
|
3329
|
+
if (item) max = Math.max(max, Number(item[1]) || 0);
|
|
3330
|
+
}
|
|
3331
|
+
return max;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
function tasksMdScope(tasksMd, revision = 0) {
|
|
3335
|
+
const raw = String(tasksMd || "");
|
|
3336
|
+
const rev = Number(revision) || 0;
|
|
3337
|
+
if (rev > 0) {
|
|
3338
|
+
const section = raw.match(
|
|
3339
|
+
new RegExp(
|
|
3340
|
+
`##\\s+Revision\\s+${rev}\\b[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s+Revision\\s+\\d+|$)`,
|
|
3341
|
+
"i",
|
|
3342
|
+
),
|
|
3343
|
+
);
|
|
3344
|
+
if (section) return section[1];
|
|
3345
|
+
// Fallback: only checkbox lines for this revision id (R1-1 / R1-01 / R1 …)
|
|
3346
|
+
return raw
|
|
3347
|
+
.split(/\r?\n/)
|
|
3348
|
+
.filter((line) =>
|
|
3349
|
+
new RegExp(`^\\s*[-*]\\s+\\[[ xX]\\]\\s+R${rev}(?:[-.\\s]|$)`, "i").test(
|
|
3350
|
+
line,
|
|
3351
|
+
),
|
|
3352
|
+
)
|
|
3353
|
+
.join("\n");
|
|
3354
|
+
}
|
|
3355
|
+
// Dispatch scope: everything before the first Revision section
|
|
3356
|
+
const cut = raw.search(/\n##\s+Revision\s+\d+\b/i);
|
|
3357
|
+
return cut >= 0 ? raw.slice(0, cut) : raw;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
function parseTasksProgress(tasksMd, { revision = 0 } = {}) {
|
|
3218
3361
|
const tasks = [];
|
|
3219
|
-
const
|
|
3362
|
+
const scope = tasksMdScope(tasksMd, revision);
|
|
3363
|
+
const lines = String(scope || "").split(/\r?\n/);
|
|
3220
3364
|
for (const line of lines) {
|
|
3221
3365
|
const m = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/);
|
|
3222
3366
|
if (!m) continue;
|
|
3223
3367
|
const done = m[1].toLowerCase() === "x";
|
|
3224
3368
|
const text = m[2].trim();
|
|
3225
|
-
const idMatch = text.match(/^(T\d+)\b/i);
|
|
3369
|
+
const idMatch = text.match(/^(T\d+|R\d+[-\w]*)\b/i);
|
|
3226
3370
|
tasks.push({
|
|
3227
3371
|
id: idMatch ? idMatch[1].toUpperCase() : `S${tasks.length + 1}`,
|
|
3228
3372
|
text,
|
|
@@ -3233,17 +3377,69 @@ function parseTasksProgress(tasksMd) {
|
|
|
3233
3377
|
const doneCount = tasks.filter((t) => t.done).length;
|
|
3234
3378
|
const next = tasks.find((t) => !t.done) || null;
|
|
3235
3379
|
let current = "暂无任务清单";
|
|
3236
|
-
if (total === 0)
|
|
3237
|
-
|
|
3238
|
-
|
|
3380
|
+
if (total === 0) {
|
|
3381
|
+
current =
|
|
3382
|
+
Number(revision) > 0
|
|
3383
|
+
? `Revision ${revision} · 等待任务清单`
|
|
3384
|
+
: "暂无任务清单";
|
|
3385
|
+
} else if (!next) {
|
|
3386
|
+
current =
|
|
3387
|
+
Number(revision) > 0
|
|
3388
|
+
? `Revision ${revision} · 任务已全部勾选 · 等待 delivery accepted`
|
|
3389
|
+
: "任务已全部勾选 · 等待 delivery accepted";
|
|
3390
|
+
} else {
|
|
3391
|
+
current = `进行中:${next.text}`;
|
|
3392
|
+
}
|
|
3239
3393
|
return {
|
|
3240
3394
|
total,
|
|
3241
3395
|
done: doneCount,
|
|
3242
3396
|
current,
|
|
3243
3397
|
tasks,
|
|
3398
|
+
revision: Number(revision) || 0,
|
|
3244
3399
|
};
|
|
3245
3400
|
}
|
|
3246
3401
|
|
|
3402
|
+
/** When delivery is accepted, never show 0/N · 工单完成 — treat checklist as complete. */
|
|
3403
|
+
function progressForAcceptedDelivery(progress, { worktreeExists = true } = {}) {
|
|
3404
|
+
const base =
|
|
3405
|
+
progress && typeof progress === "object"
|
|
3406
|
+
? progress
|
|
3407
|
+
: { total: 0, done: 0, current: "", tasks: [] };
|
|
3408
|
+
const tasks = Array.isArray(base.tasks)
|
|
3409
|
+
? base.tasks.map((t) => ({ ...t, done: true }))
|
|
3410
|
+
: [];
|
|
3411
|
+
const total = Number(base.total) || tasks.length;
|
|
3412
|
+
return {
|
|
3413
|
+
...base,
|
|
3414
|
+
total,
|
|
3415
|
+
done: total,
|
|
3416
|
+
tasks,
|
|
3417
|
+
current: worktreeExists
|
|
3418
|
+
? "delivery accepted · 工单完成"
|
|
3419
|
+
: "delivery accepted · 已合入 develop(worktree 已清理)",
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
/** Idempotent: flip remaining `- [ ]` to `- [x]` once delivery is accepted. */
|
|
3424
|
+
function reconcileTasksMdOnAccept(tasksPath) {
|
|
3425
|
+
if (!tasksPath || !fs.existsSync(tasksPath)) return false;
|
|
3426
|
+
let raw;
|
|
3427
|
+
try {
|
|
3428
|
+
raw = fs.readFileSync(tasksPath, "utf8");
|
|
3429
|
+
} catch {
|
|
3430
|
+
return false;
|
|
3431
|
+
}
|
|
3432
|
+
if (!/^\s*[-*]\s+\[\s\]\s+/m.test(raw)) return false;
|
|
3433
|
+
const next = raw.replace(/^(\s*[-*]\s+)\[\s\](\s+)/gm, "$1[x]$2");
|
|
3434
|
+
if (next === raw) return false;
|
|
3435
|
+
try {
|
|
3436
|
+
fs.writeFileSync(tasksPath, next, "utf8");
|
|
3437
|
+
return true;
|
|
3438
|
+
} catch {
|
|
3439
|
+
return false;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3247
3443
|
function readLogTail(logPath, maxLines = 12) {
|
|
3248
3444
|
if (!logPath || !fs.existsSync(logPath)) return [];
|
|
3249
3445
|
try {
|
|
@@ -3406,12 +3602,12 @@ function dispatchStatus(jobId) {
|
|
|
3406
3602
|
}
|
|
3407
3603
|
}
|
|
3408
3604
|
const tasksPath = featureDir ? path.join(featureDir, "tasks.md") : null;
|
|
3409
|
-
let
|
|
3605
|
+
let tasksRaw = "";
|
|
3410
3606
|
if (tasksPath && fs.existsSync(tasksPath)) {
|
|
3411
3607
|
try {
|
|
3412
|
-
|
|
3608
|
+
tasksRaw = fs.readFileSync(tasksPath, "utf8");
|
|
3413
3609
|
} catch {
|
|
3414
|
-
|
|
3610
|
+
tasksRaw = "";
|
|
3415
3611
|
}
|
|
3416
3612
|
}
|
|
3417
3613
|
const logPath =
|
|
@@ -3419,13 +3615,61 @@ function dispatchStatus(jobId) {
|
|
|
3419
3615
|
(featureDir ? path.join(featureDir, "agent-launch.log") : null);
|
|
3420
3616
|
const logTail = logPath ? readLogTail(logPath) : [];
|
|
3421
3617
|
const worktreeExists = roots.worktreeExists;
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3618
|
+
const terminal = terminalQueueSnapshot(dispatch.worktreePath);
|
|
3619
|
+
const inferredFromTasks = inferRevisionFromTasksMd(tasksRaw);
|
|
3620
|
+
const revisionHint = Math.max(
|
|
3621
|
+
Number(live.job.revisionCount || 0),
|
|
3622
|
+
Number(delivery?.revision || 0),
|
|
3623
|
+
inferredFromTasks,
|
|
3624
|
+
);
|
|
3625
|
+
const revProgressHint =
|
|
3626
|
+
revisionHint > 0
|
|
3627
|
+
? parseTasksProgress(tasksRaw, { revision: revisionHint })
|
|
3628
|
+
: null;
|
|
3629
|
+
const hasOpenRevWork =
|
|
3630
|
+
Boolean(revProgressHint) &&
|
|
3631
|
+
revProgressHint.total > 0 &&
|
|
3632
|
+
revProgressHint.done < revProgressHint.total;
|
|
3633
|
+
// First-dispatch delivery is also "open" — only treat as revise when a
|
|
3634
|
+
// Revision N already exists (job / delivery / tasks.md), or Terminal is
|
|
3635
|
+
// busy on open R{n} work while status still lags on accepted.
|
|
3636
|
+
const deliveryAccepted = delivery?.status === "accepted";
|
|
3637
|
+
const deliveryOpen = delivery?.status === "open";
|
|
3638
|
+
const activelyRevising =
|
|
3639
|
+
(!deliveryAccepted && live.job.status === "revising") ||
|
|
3640
|
+
(revisionHint > 0 && deliveryOpen) ||
|
|
3641
|
+
(hasOpenRevWork &&
|
|
3642
|
+
(terminal.busy || Number(terminal.queueDepth || 0) > 0));
|
|
3643
|
+
const activeRevision =
|
|
3644
|
+
revisionHint > 0 &&
|
|
3645
|
+
(activelyRevising ||
|
|
3646
|
+
Number(live.job.revisionCount || 0) > 0 ||
|
|
3647
|
+
Number(delivery?.revision || 0) > 0 ||
|
|
3648
|
+
inferredFromTasks > 0)
|
|
3649
|
+
? revisionHint
|
|
3650
|
+
: 0;
|
|
3651
|
+
|
|
3652
|
+
let progress = parseTasksProgress(tasksRaw, {
|
|
3653
|
+
revision: activeRevision > 0 ? activeRevision : 0,
|
|
3654
|
+
});
|
|
3655
|
+
const accepted = deliveryAccepted && !activelyRevising;
|
|
3656
|
+
if (accepted) {
|
|
3657
|
+
if (tasksPath) reconcileTasksMdOnAccept(tasksPath);
|
|
3658
|
+
if (tasksPath && fs.existsSync(tasksPath)) {
|
|
3659
|
+
try {
|
|
3660
|
+
tasksRaw = fs.readFileSync(tasksPath, "utf8");
|
|
3661
|
+
} catch {
|
|
3662
|
+
// keep
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
progress = parseTasksProgress(tasksRaw, {
|
|
3666
|
+
revision: activeRevision > 0 ? activeRevision : 0,
|
|
3667
|
+
});
|
|
3668
|
+
progress = progressForAcceptedDelivery(progress, { worktreeExists });
|
|
3669
|
+
} else if (activelyRevising && progress.total === 0 && activeRevision > 0) {
|
|
3424
3670
|
progress = {
|
|
3425
3671
|
...progress,
|
|
3426
|
-
current:
|
|
3427
|
-
? "delivery accepted · 工单完成"
|
|
3428
|
-
: "delivery accepted · 已合入 develop(worktree 已清理)",
|
|
3672
|
+
current: `Revision ${activeRevision} · 已续派,等待任务勾选…`,
|
|
3429
3673
|
};
|
|
3430
3674
|
} else if (!worktreeExists && roots.source === "primary") {
|
|
3431
3675
|
progress = {
|
|
@@ -3435,6 +3679,36 @@ function dispatchStatus(jobId) {
|
|
|
3435
3679
|
"worktree 已清理;成品与 Brief 在产品仓 develop",
|
|
3436
3680
|
};
|
|
3437
3681
|
}
|
|
3682
|
+
|
|
3683
|
+
// Keep live job revisionCount in sync when Brief/tasks already show Revision N
|
|
3684
|
+
if (
|
|
3685
|
+
activeRevision > Number(live.job.revisionCount || 0) ||
|
|
3686
|
+
(activelyRevising && live.job.status !== "revising")
|
|
3687
|
+
) {
|
|
3688
|
+
try {
|
|
3689
|
+
const nextJob = {
|
|
3690
|
+
...live.job,
|
|
3691
|
+
revisionCount: Math.max(
|
|
3692
|
+
Number(live.job.revisionCount || 0),
|
|
3693
|
+
activeRevision,
|
|
3694
|
+
),
|
|
3695
|
+
status: activelyRevising
|
|
3696
|
+
? "revising"
|
|
3697
|
+
: accepted
|
|
3698
|
+
? "accepted"
|
|
3699
|
+
: live.job.status,
|
|
3700
|
+
};
|
|
3701
|
+
fs.writeFileSync(
|
|
3702
|
+
live.jobPath,
|
|
3703
|
+
`${JSON.stringify(nextJob, null, 2)}\n`,
|
|
3704
|
+
"utf8",
|
|
3705
|
+
);
|
|
3706
|
+
live.job = nextJob;
|
|
3707
|
+
} catch {
|
|
3708
|
+
// ignore
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3438
3712
|
const preview = resolvePreview({
|
|
3439
3713
|
delivery: delivery || { status: "open" },
|
|
3440
3714
|
worktreePath: roots.root,
|
|
@@ -3443,8 +3717,10 @@ function dispatchStatus(jobId) {
|
|
|
3443
3717
|
// Only after accept (or during a revise round). Not while first dispatch is still in progress.
|
|
3444
3718
|
const showPreview =
|
|
3445
3719
|
accepted ||
|
|
3720
|
+
activelyRevising ||
|
|
3446
3721
|
live.job.status === "revising" ||
|
|
3447
|
-
Number(live.job.revisionCount || 0) > 0
|
|
3722
|
+
Number(live.job.revisionCount || 0) > 0 ||
|
|
3723
|
+
activeRevision > 0;
|
|
3448
3724
|
|
|
3449
3725
|
// Persist accepted status when handoff moved Brief to primary
|
|
3450
3726
|
if (accepted && live.job.status !== "accepted" && roots.source === "primary") {
|
|
@@ -3455,6 +3731,7 @@ function dispatchStatus(jobId) {
|
|
|
3455
3731
|
`${JSON.stringify(nextJob, null, 2)}\n`,
|
|
3456
3732
|
"utf8",
|
|
3457
3733
|
);
|
|
3734
|
+
live.job = nextJob;
|
|
3458
3735
|
} catch {
|
|
3459
3736
|
// ignore
|
|
3460
3737
|
}
|
|
@@ -3463,12 +3740,20 @@ function dispatchStatus(jobId) {
|
|
|
3463
3740
|
const canRevise =
|
|
3464
3741
|
Boolean(dispatch.repoPath && fs.existsSync(dispatch.repoPath)) &&
|
|
3465
3742
|
(accepted ||
|
|
3743
|
+
activelyRevising ||
|
|
3466
3744
|
live.job.status === "revising" ||
|
|
3467
|
-
Number(live.job.revisionCount || 0) > 0
|
|
3745
|
+
Number(live.job.revisionCount || 0) > 0 ||
|
|
3746
|
+
activeRevision > 0);
|
|
3747
|
+
|
|
3748
|
+
const statusOut = activelyRevising
|
|
3749
|
+
? "revising"
|
|
3750
|
+
: accepted
|
|
3751
|
+
? "accepted"
|
|
3752
|
+
: live.job.status;
|
|
3468
3753
|
|
|
3469
3754
|
return {
|
|
3470
3755
|
jobId: live.id,
|
|
3471
|
-
status:
|
|
3756
|
+
status: statusOut,
|
|
3472
3757
|
dispatch: {
|
|
3473
3758
|
...dispatch,
|
|
3474
3759
|
worktreeExists,
|
|
@@ -3480,8 +3765,13 @@ function dispatchStatus(jobId) {
|
|
|
3480
3765
|
logTail,
|
|
3481
3766
|
preview: showPreview ? preview : null,
|
|
3482
3767
|
canRevise,
|
|
3483
|
-
revision:
|
|
3768
|
+
revision: Math.max(
|
|
3769
|
+
Number(live.job.revisionCount || 0),
|
|
3770
|
+
activeRevision,
|
|
3771
|
+
Number(delivery?.revision || 0),
|
|
3772
|
+
),
|
|
3484
3773
|
handoffCleaned: !worktreeExists && roots.source === "primary",
|
|
3774
|
+
terminal,
|
|
3485
3775
|
};
|
|
3486
3776
|
}
|
|
3487
3777
|
|
|
@@ -3932,6 +4222,8 @@ async function handleApi(req, res) {
|
|
|
3932
4222
|
send(res, 422, {
|
|
3933
4223
|
ok: false,
|
|
3934
4224
|
passed: false,
|
|
4225
|
+
code: "VALIDATE_GATE",
|
|
4226
|
+
retryable: true,
|
|
3935
4227
|
error: err.message,
|
|
3936
4228
|
summary: review.summary || err.message,
|
|
3937
4229
|
issues: review.issues || [],
|
|
@@ -3944,8 +4236,28 @@ async function handleApi(req, res) {
|
|
|
3944
4236
|
});
|
|
3945
4237
|
return;
|
|
3946
4238
|
}
|
|
3947
|
-
|
|
4239
|
+
const code =
|
|
4240
|
+
err?.code === "PREEMPT_FAILED"
|
|
4241
|
+
? "PREEMPT_FAILED"
|
|
4242
|
+
: err?.code === "CHILD_TIMEOUT"
|
|
4243
|
+
? "CHILD_TIMEOUT"
|
|
4244
|
+
: err?.code === "NO_AGENT" || err?.code === "AGENT_MISSING"
|
|
4245
|
+
? err.code
|
|
4246
|
+
: err?.name === "LaunchGateError"
|
|
4247
|
+
? err.code || "LAUNCH_FAILED"
|
|
4248
|
+
: err?.code || "REVISE_FAILED";
|
|
4249
|
+
const status =
|
|
4250
|
+
code === "CHILD_TIMEOUT"
|
|
4251
|
+
? 504
|
|
4252
|
+
: code === "PREEMPT_FAILED"
|
|
4253
|
+
? 409
|
|
4254
|
+
: 400;
|
|
4255
|
+
send(res, status, {
|
|
4256
|
+
ok: false,
|
|
4257
|
+
code,
|
|
4258
|
+
retryable: err?.retryable !== false,
|
|
3948
4259
|
error: err instanceof Error ? err.message : "revise failed",
|
|
4260
|
+
detail: err?.detail || undefined,
|
|
3949
4261
|
});
|
|
3950
4262
|
}
|
|
3951
4263
|
return;
|
|
@@ -67,6 +67,10 @@ the table for their app.
|
|
|
67
67
|
| E2E-052 | Confirm column wider than chat; requirements (and revise) fields fully expanded without inner scroll; structured paragraphs/lists display with click-to-edit | manual |
|
|
68
68
|
| E2E-053 | Full-screen desk (no 1440 shell cap); chat column ~50px wider than prior confirm-wide min; confirm still wider than chat | manual |
|
|
69
69
|
| E2E-054 | Live L3 smoke: `npm run test:live` covers desk shell markers + validate fail/pass + validate/fix via mock LLM (no paid API) | `npm run test:live` |
|
|
70
|
+
| E2E-055 | Revise reliability: preempt failure returns `PREEMPT_FAILED` without locking card; failed launch rolls back Brief Revision; status shows Terminal busy/queue; retry stays available | `npm test` + manual |
|
|
71
|
+
| E2E-056 | Progress on accept: status never shows `0/N · delivery accepted`; unchecked tasks reconciled to `[x]` | `npm test` |
|
|
72
|
+
| E2E-057 | After Confirm revise, progress tracks this revision’s `R{n}-*` tasks (not prior T* / frozen accepted); busy Terminal + lagging revisionCount still scopes correctly | `npm test` + manual |
|
|
73
|
+
| E2E-058 | priorAccepted revise: preempt clears leftovers then enqueue; false `PREEMPT_FAILED` must not appear while that revise Agent is already running; hard fail leaves nothing queued | `npm test` + manual |
|
|
70
74
|
| E2E-041 | Busy Terminal runner: revise/re-dispatch queues (`pending.cmd`), waits (no interrupt); API `queued`/`busy`/`reused`; UI does not claim agent already started | manual |
|
|
71
75
|
| E2E-007 | Request worktrees use `.worktree/<id>`; `.worktree/` gitignored | manual |
|
|
72
76
|
| E2E-008 | Docs require main+develop+feat+fix and typed go-live flows | manual |
|
|
@@ -128,3 +132,6 @@ the table for their app.
|
|
|
128
132
|
| E2E-039 | `038-revise-enqueue-style` | honest enqueue + dark revise card |
|
|
129
133
|
| E2E-040 | `039-desk-scroll-sync` | left/right viewport sync |
|
|
130
134
|
| E2E-041 | `040-terminal-queue-wait` | busy queue wait + honest API/UI |
|
|
135
|
+
| E2E-056 | `057-progress-accepted` | accept never shows 0/N |
|
|
136
|
+
| E2E-057 | `058-revise-progress-track` | revise scopes R{n} progress |
|
|
137
|
+
| E2E-058 | `059-revise-preempt-order` | preempt before enqueue |
|
package/package.json
CHANGED
package/web/live-dev/app.js
CHANGED
|
@@ -36,6 +36,8 @@ const state = {
|
|
|
36
36
|
mode: "specify", // specify | revise
|
|
37
37
|
/** After a successful revise: right card stays locked 改进卡. */
|
|
38
38
|
reviseLocked: false,
|
|
39
|
+
/** Revising but Terminal busy with no task progress — offer retry CTA. */
|
|
40
|
+
reviseStuckHint: false,
|
|
39
41
|
lastRevision: null, // { revision, change, keep, acceptance, reason }
|
|
40
42
|
originalCard: null, // snapshot after first confirm
|
|
41
43
|
lastStatus: null,
|
|
@@ -612,7 +614,9 @@ function syncReviseDispatchButton(ready) {
|
|
|
612
614
|
const accepted = state.lastDeliveryAccepted || status === "accepted";
|
|
613
615
|
const revising = status === "revising";
|
|
614
616
|
if (el.reviseHint) {
|
|
615
|
-
if (revising && !dialoguing) {
|
|
617
|
+
if (revising && state.reviseStuckHint && !dialoguing) {
|
|
618
|
+
el.reviseHint.textContent = t("revise.hintStuck");
|
|
619
|
+
} else if (revising && !dialoguing) {
|
|
616
620
|
el.reviseHint.textContent = t("revise.hintRevising");
|
|
617
621
|
} else if (state.reviseLocked && !dialoguing) {
|
|
618
622
|
el.reviseHint.textContent = t("revise.hintLocked");
|
|
@@ -629,7 +633,8 @@ function syncReviseDispatchButton(ready) {
|
|
|
629
633
|
}
|
|
630
634
|
}
|
|
631
635
|
if (el.startReviseChat || el.startReviseChatAlt) {
|
|
632
|
-
const showCta =
|
|
636
|
+
const showCta =
|
|
637
|
+
!dialoguing && accepted && (!revising || state.reviseStuckHint);
|
|
633
638
|
const previewVisible = el.previewPanel && !el.previewPanel.hidden;
|
|
634
639
|
if (el.startReviseChat) {
|
|
635
640
|
el.startReviseChat.hidden = !(showCta && previewVisible);
|
|
@@ -2204,7 +2209,8 @@ async function confirmReviseAndDispatch() {
|
|
|
2204
2209
|
agentId: state.agentId,
|
|
2205
2210
|
};
|
|
2206
2211
|
const ac = new AbortController();
|
|
2207
|
-
|
|
2212
|
+
// Cover recreate (git ≤60s) + optional duaer init (≤120s) + validate/launch headroom.
|
|
2213
|
+
const timer = setTimeout(() => ac.abort(), 180000);
|
|
2208
2214
|
const res = await fetch("/api/revise", {
|
|
2209
2215
|
method: "POST",
|
|
2210
2216
|
headers: { "content-type": "application/json" },
|
|
@@ -2218,10 +2224,13 @@ async function confirmReviseAndDispatch() {
|
|
|
2218
2224
|
data = rawText ? JSON.parse(rawText) : {};
|
|
2219
2225
|
} catch {
|
|
2220
2226
|
const snippet = String(rawText || "").replace(/\s+/g, " ").slice(0, 180);
|
|
2221
|
-
throw
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2227
|
+
throw Object.assign(
|
|
2228
|
+
new Error(
|
|
2229
|
+
snippet
|
|
2230
|
+
? `HTTP ${res.status}: ${snippet}`
|
|
2231
|
+
: `HTTP ${res.status} ${t("err.revise")}`,
|
|
2232
|
+
),
|
|
2233
|
+
{ retryable: true },
|
|
2225
2234
|
);
|
|
2226
2235
|
}
|
|
2227
2236
|
if (res.status === 422 || data.passed === false) {
|
|
@@ -2238,7 +2247,12 @@ async function confirmReviseAndDispatch() {
|
|
|
2238
2247
|
syncConfirmEnabled();
|
|
2239
2248
|
return;
|
|
2240
2249
|
}
|
|
2241
|
-
if (!res.ok)
|
|
2250
|
+
if (!res.ok) {
|
|
2251
|
+
const err = new Error(reviseErrorMessage(data, res.status));
|
|
2252
|
+
err.retryable = data.retryable !== false;
|
|
2253
|
+
err.code = data.code || "";
|
|
2254
|
+
throw err;
|
|
2255
|
+
}
|
|
2242
2256
|
resetValidateGate();
|
|
2243
2257
|
const restated = data.restated
|
|
2244
2258
|
? t("bot.reviseRestate", {
|
|
@@ -2273,23 +2287,49 @@ async function confirmReviseAndDispatch() {
|
|
|
2273
2287
|
}
|
|
2274
2288
|
startStatusPoll();
|
|
2275
2289
|
} catch (err) {
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2290
|
+
const timedOut = err?.name === "AbortError";
|
|
2291
|
+
const msg = timedOut
|
|
2292
|
+
? t("err.reviseTimeout")
|
|
2293
|
+
: err instanceof Error
|
|
2294
|
+
? err.message
|
|
2295
|
+
: t("err.revise");
|
|
2296
|
+
const retryable = timedOut || err?.retryable !== false;
|
|
2282
2297
|
if (el.reviseErr) {
|
|
2283
2298
|
el.reviseErr.hidden = false;
|
|
2284
|
-
el.reviseErr.textContent =
|
|
2299
|
+
el.reviseErr.textContent = retryable
|
|
2300
|
+
? `${msg} ${t("err.retryableHint")}`
|
|
2301
|
+
: msg;
|
|
2285
2302
|
}
|
|
2286
2303
|
addBubble("bot", msg);
|
|
2304
|
+
// Keep card unlocked so Confirm revise stays available.
|
|
2305
|
+
state.reviseLocked = false;
|
|
2306
|
+
setReviseFieldsReadonly(false);
|
|
2287
2307
|
} finally {
|
|
2288
2308
|
state.reviseDispatching = false;
|
|
2289
2309
|
syncConfirmEnabled();
|
|
2290
2310
|
}
|
|
2291
2311
|
}
|
|
2292
2312
|
|
|
2313
|
+
function reviseErrorMessage(data, status) {
|
|
2314
|
+
const code = String(data?.code || "").trim();
|
|
2315
|
+
if (code) {
|
|
2316
|
+
const keyed = t(`err.code.${code}`);
|
|
2317
|
+
if (keyed && keyed !== `err.code.${code}`) return keyed;
|
|
2318
|
+
}
|
|
2319
|
+
return data?.error || `HTTP ${status} ${t("err.revise")}`;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function formatTerminalStatus(term) {
|
|
2323
|
+
if (!term || typeof term !== "object") return "";
|
|
2324
|
+
if (!term.runnerHealthy) return t("status.terminalDown");
|
|
2325
|
+
const parts = [
|
|
2326
|
+
term.busy ? t("status.terminalBusy") : t("status.terminalIdle"),
|
|
2327
|
+
];
|
|
2328
|
+
const depth = Number(term.queueDepth || 0);
|
|
2329
|
+
if (depth > 0) parts.push(t("status.terminalQueue", { n: depth }));
|
|
2330
|
+
return parts.join(" · ");
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2293
2333
|
function startStatusPoll() {
|
|
2294
2334
|
if (state.statusTimer) clearInterval(state.statusTimer);
|
|
2295
2335
|
let acceptedNotified = false;
|
|
@@ -2317,6 +2357,25 @@ function startStatusPoll() {
|
|
|
2317
2357
|
? t("status.worktreeGone")
|
|
2318
2358
|
: "",
|
|
2319
2359
|
});
|
|
2360
|
+
const termLine = formatTerminalStatus(data.terminal);
|
|
2361
|
+
if (termLine) {
|
|
2362
|
+
el.dispatchStatus.textContent = `${el.dispatchStatus.textContent} · ${t(
|
|
2363
|
+
"status.terminalLine",
|
|
2364
|
+
{ state: termLine },
|
|
2365
|
+
)}`;
|
|
2366
|
+
}
|
|
2367
|
+
// If revising but Terminal is stuck busy with no progress, keep「再改一版」visible.
|
|
2368
|
+
if (
|
|
2369
|
+
data.status === "revising" &&
|
|
2370
|
+
data.terminal?.busy &&
|
|
2371
|
+
data.terminal?.runnerHealthy &&
|
|
2372
|
+
!(data.progress?.done > 0)
|
|
2373
|
+
) {
|
|
2374
|
+
state.reviseStuckHint = true;
|
|
2375
|
+
} else {
|
|
2376
|
+
state.reviseStuckHint = false;
|
|
2377
|
+
}
|
|
2378
|
+
renderRevisePanel(data);
|
|
2320
2379
|
if (data.status === "accepted" && data.delivery?.status === "accepted") {
|
|
2321
2380
|
el.dispatchStatus.textContent = t("status.acceptedRevise", { rev });
|
|
2322
2381
|
if (!acceptedNotified || data.revision !== lastRevision) {
|
package/web/live-dev/i18n.js
CHANGED
|
@@ -112,6 +112,8 @@ const zhCN = {
|
|
|
112
112
|
"revise.hintEnqueue": "正在送入同一 Terminal(不新开窗口)…",
|
|
113
113
|
"revise.hintLocked": "本轮改进卡(下方)已确认。改完验收后若仍不满意,再点「再改一版」。",
|
|
114
114
|
"revise.hintRevising": "数字员工改写中。可继续「查看成品」;验收后再点「再改一版」。",
|
|
115
|
+
"revise.hintStuck":
|
|
116
|
+
"Terminal 仍忙且尚无任务进度。可结束旧任务后点「再改一版」重试续派。",
|
|
115
117
|
"bot.ready":
|
|
116
118
|
"模型已就绪。随便说你想做什么;我会多轮问清,右侧是确认卡。确认前不会改你的业务仓库。",
|
|
117
119
|
"bot.cardReady": "右侧确认卡可再改。满意后点「需求无误,开始干活」。",
|
|
@@ -222,6 +224,20 @@ const zhCN = {
|
|
|
222
224
|
"err.reviseKickoff": "改进对话启动失败",
|
|
223
225
|
"err.confirm": "确认失败",
|
|
224
226
|
"err.timeout": "超时。请刷新重试;若 Agent 已打开可在 Terminal 里继续。",
|
|
227
|
+
"err.reviseTimeout":
|
|
228
|
+
"续派超时(约 3 分钟)。Brief 未写入本轮 Revision 时可直接再点「再派一版」;若 Terminal 已有任务请在窗口里继续。",
|
|
229
|
+
"err.code.PREEMPT_FAILED":
|
|
230
|
+
"无法抢占仍在运行的 Agent。请结束该 worktree 的 Terminal 任务后,再点「再派一版」。",
|
|
231
|
+
"err.code.CHILD_TIMEOUT": "子进程超时被中止。请重试续派;若 git/init 卡住请检查仓库与钩子。",
|
|
232
|
+
"err.code.LAUNCH_FAILED": "续派启动失败。可直接再点「再派一版」重试。",
|
|
233
|
+
"err.code.NO_AGENT": "未检测到可用 CLI。请安装 Cursor Agent 或 Claude Code 后重试。",
|
|
234
|
+
"err.code.AGENT_MISSING": "所选 CLI 未安装。请安装后重试。",
|
|
235
|
+
"err.retryableHint": "可以再试一次。",
|
|
236
|
+
"status.terminalLine": "Terminal:{state}",
|
|
237
|
+
"status.terminalBusy": "忙碌中",
|
|
238
|
+
"status.terminalIdle": "空闲",
|
|
239
|
+
"status.terminalQueue": "队列 {n}",
|
|
240
|
+
"status.terminalDown": "无 runner",
|
|
225
241
|
"err.installAgent": "请先安装:{cmd}",
|
|
226
242
|
"err.needAgent": "请先安装所选 CLI",
|
|
227
243
|
"err.needAgentAlt": "请先安装对应 CLI",
|
|
@@ -342,6 +358,8 @@ const en = {
|
|
|
342
358
|
"This revise card is locked. After accept, tap Revise again if needed.",
|
|
343
359
|
"revise.hintRevising":
|
|
344
360
|
"Employee is revising. You can View product; Revise again after accept.",
|
|
361
|
+
"revise.hintStuck":
|
|
362
|
+
"Terminal is still busy with no task progress. Stop the old job, then tap Revise again to retry.",
|
|
345
363
|
"bot.ready":
|
|
346
364
|
"Model ready. Tell me what you want; I will clarify in chat. The confirm card is on the right. Nothing touches your product repo until you confirm.",
|
|
347
365
|
"bot.cardReady":
|
|
@@ -453,6 +471,21 @@ const en = {
|
|
|
453
471
|
"err.reviseKickoff": "Could not start revise chat",
|
|
454
472
|
"err.confirm": "Confirm failed",
|
|
455
473
|
"err.timeout": "Timed out. Refresh; if the Agent is open, continue in Terminal.",
|
|
474
|
+
"err.reviseTimeout":
|
|
475
|
+
"Revise timed out (~3 min). If this Revision was not written, tap Confirm revise again; if Terminal already has a job, continue there.",
|
|
476
|
+
"err.code.PREEMPT_FAILED":
|
|
477
|
+
"Could not preempt a running Agent. Stop that worktree Terminal job, then tap Confirm revise again.",
|
|
478
|
+
"err.code.CHILD_TIMEOUT":
|
|
479
|
+
"A child process timed out. Retry revise; check git hooks if checkout/init stalls.",
|
|
480
|
+
"err.code.LAUNCH_FAILED": "Revise launch failed. Tap Confirm revise to retry.",
|
|
481
|
+
"err.code.NO_AGENT": "No CLI found. Install Cursor Agent or Claude Code, then retry.",
|
|
482
|
+
"err.code.AGENT_MISSING": "Selected CLI is not installed. Install it, then retry.",
|
|
483
|
+
"err.retryableHint": "You can retry.",
|
|
484
|
+
"status.terminalLine": "Terminal: {state}",
|
|
485
|
+
"status.terminalBusy": "busy",
|
|
486
|
+
"status.terminalIdle": "idle",
|
|
487
|
+
"status.terminalQueue": "queue {n}",
|
|
488
|
+
"status.terminalDown": "no runner",
|
|
456
489
|
"err.installAgent": "Install first: {cmd}",
|
|
457
490
|
"err.needAgent": "Install the selected CLI first",
|
|
458
491
|
"err.needAgentAlt": "Install the matching CLI first",
|