duaer-spec 0.2.0 → 0.4.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/.cursor/rules/duaer-spec.mdc +15 -22
- package/.cursor/skills/duaer-converge/SKILL.md +7 -4
- package/.cursor/skills/duaer-do/SKILL.md +62 -0
- package/.cursor/skills/duaer-specify/SKILL.md +13 -1
- package/.duaer/delivery-policy.json +6 -0
- package/.duaer/memory/constitution.md +9 -9
- package/ADOPT.md +20 -52
- package/CHANGELOG.md +28 -38
- package/DUADER.md +27 -35
- package/README.md +28 -79
- package/bin/duaer.mjs +287 -131
- package/docs/agent/change-checklist.md +2 -2
- package/docs/agent/e2e-test-plan.md +16 -10
- package/package.json +2 -2
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Prefer simple /duaer-do; Spec before code; handoff before claiming done
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Duaer
|
|
6
|
+
# Duaer (keep it simple)
|
|
7
7
|
|
|
8
|
-
Treat agents as digital employees
|
|
9
|
-
only via converge. See `DUADER.md` and `.duaer/memory/constitution.md`.
|
|
8
|
+
Treat agents as digital employees. Prefer **one** everyday command:
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
Issue/PR), **AGENTS.md wins**.
|
|
10
|
+
`/duaer-do <ask>` → then `duaer status` if needed.
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
If this conflicts with `AGENTS.md` (worktrees, commits, Issue/PR), **AGENTS.md wins**.
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
## Everyday
|
|
17
15
|
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
1. `/duaer-do` — assign Brief, implement, converge
|
|
17
|
+
2. Do **not** claim "done" unless the job is accepted (`duaer status`)
|
|
18
|
+
3. Do **not** make the user chain specify → plan → tasks → implement → converge for normal asks
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
## When to use step skills
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
- `/duaer-specify` only — reshape the Brief
|
|
23
|
+
- `/duaer-plan` / `/duaer-tasks` — large or architectural work
|
|
24
|
+
- `/duaer-implement` / `/duaer-converge` — resume mid-job
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Hotfix: still a short Spec + accept (or `/duaer-do`).
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`/duaer-specify` (mark hotfix: symptom / root cause / acceptance) → `/duaer-tasks` → `/duaer-implement` → `/duaer-converge`
|
|
31
|
-
|
|
32
|
-
- Implement only from `tasks.md` when it exists
|
|
33
|
-
- Update `.duaer/memory/project-context.md` when architecture changes
|
|
34
|
-
- Run the verification named in the feature docs before merge
|
|
35
|
-
- Before merge: `duaer check . --gate` (Spec present, no open tasks, `delivery.json` accepted)
|
|
28
|
+
Forbidden: jump from chat to code with no Spec under `.duaer/specs/`.
|
|
@@ -265,15 +265,18 @@ Append to the **end** of `tasks.md`, per the append contract:
|
|
|
265
265
|
path instead — do not stamp accepted).
|
|
266
266
|
- Report: **"✅ Converged — the implementation satisfies the spec, plan, and tasks."**
|
|
267
267
|
- Include the summary counts of what was checked.
|
|
268
|
-
- Tell the user they can run `duaer
|
|
268
|
+
- Tell the user they can run `duaer job .` to see handoff status (policy default: coach).
|
|
269
|
+
- Reminder: this is **job acceptance**, not a repository merge lock.
|
|
269
270
|
### 8. Provide Next Actions (Handoff)
|
|
270
271
|
|
|
271
272
|
- On `tasks_appended`: state how many tasks were appended under which phase, and recommend
|
|
272
273
|
running `/duaer-implement` to complete them; note that a follow-up converge
|
|
273
274
|
run will find fewer or no remaining items. Remind: `delivery.json` is `open` —
|
|
274
|
-
`duaer
|
|
275
|
-
- On `converged`: recommend `duaer
|
|
276
|
-
implement pass is needed for this feature's specified scope.
|
|
275
|
+
under coach/strict, do **not** claim the job is done; `duaer job .` will show unfinished.
|
|
276
|
+
- On `converged`: recommend `duaer job .`, then ask the user to review. No further
|
|
277
|
+
implement pass is needed for this feature's specified scope. Use the handoff script:
|
|
278
|
+
"✅ Job accepted — Brief satisfied per converge; ready for your review."
|
|
279
|
+
Do not frame this as a git/CI merge requirement.
|
|
277
280
|
|
|
278
281
|
### 9. Check for extension hooks
|
|
279
282
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "duaer-do"
|
|
3
|
+
description: "Simple one-shot: turn a user ask into an assigned job, implement it, and hand off. Prefer this for everyday work instead of chaining specify/plan/tasks/implement/converge."
|
|
4
|
+
compatibility: "Requires duaer-spec project structure with .duaer/ directory"
|
|
5
|
+
metadata:
|
|
6
|
+
author: "duaer-spec"
|
|
7
|
+
source: "duaer-spec"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## User Input
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
$ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
You **MUST** use the user input as the job request (if not empty).
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
**Everyday path — keep it simple.** Run one digital-employee job end-to-end without
|
|
21
|
+
asking the user to chain slash commands.
|
|
22
|
+
|
|
23
|
+
Do **not** lecture about methodology. Do the work.
|
|
24
|
+
|
|
25
|
+
## Steps (always in order)
|
|
26
|
+
|
|
27
|
+
1. **Assign** — Ensure an active feature Brief exists:
|
|
28
|
+
- If there is already a clear active job (`.duaer/active-job.json` + `spec.md`)
|
|
29
|
+
that matches this ask, update that `spec.md` acceptance as needed.
|
|
30
|
+
- Otherwise run the same outcome as `/duaer-specify` for this ask
|
|
31
|
+
(create `.duaer/specs/<nnn-slug>/spec.md`, write `.duaer/active-job.json`).
|
|
32
|
+
- For small asks: keep the Spec short (what / why / acceptance only).
|
|
33
|
+
|
|
34
|
+
2. **Break down (lightweight)** — If `tasks.md` is missing:
|
|
35
|
+
- Write a short `tasks.md` with a few `- [ ]` items from the Spec.
|
|
36
|
+
- Skip `/duaer-plan` unless architecture or contracts clearly change.
|
|
37
|
+
- If `tasks.md` already exists, use it.
|
|
38
|
+
|
|
39
|
+
3. **Work** — Implement open tasks (same rules as `/duaer-implement`).
|
|
40
|
+
Mark tasks `[x]` as you finish them. Do not expand scope past the Spec.
|
|
41
|
+
|
|
42
|
+
4. **Accept** — Run the same outcome as `/duaer-converge`:
|
|
43
|
+
- Compare code to Spec; append gap tasks or stamp `delivery.json` accepted.
|
|
44
|
+
- If gaps were appended, either implement them now (preferred for small jobs)
|
|
45
|
+
or stop and tell the user what remains — once.
|
|
46
|
+
|
|
47
|
+
5. **Handoff line** — End with exactly one clear status:
|
|
48
|
+
- If accepted: `✅ Job accepted — ready for your review.`
|
|
49
|
+
- If not: `⏳ Job not accepted yet — <one sentence what to do next>.`
|
|
50
|
+
- Do **not** claim "done" unless `delivery.json` is `accepted` and tasks are clear
|
|
51
|
+
(coach/strict policy). Git is not locked.
|
|
52
|
+
|
|
53
|
+
## Hotfix
|
|
54
|
+
|
|
55
|
+
If the user says hotfix/bug/regression: still write a short Spec (symptom / cause /
|
|
56
|
+
acceptance), then tasks → implement → converge. Same handoff line.
|
|
57
|
+
|
|
58
|
+
## Out of scope for this command
|
|
59
|
+
|
|
60
|
+
- Do not require the user to run `/duaer-plan`, `/duaer-checklist`, or `/duaer-analyze`.
|
|
61
|
+
- Do not discuss policy modes unless they ask.
|
|
62
|
+
- Do not turn this into a merge/CI lecture.
|
|
@@ -104,11 +104,23 @@ Given that feature description, do this:
|
|
|
104
104
|
Write the actual resolved directory path value (for example, `specs/003-user-auth`), not the literal string `SPECIFY_FEATURE_DIRECTORY`.
|
|
105
105
|
This allows downstream commands (`/duaer-plan`, `/duaer-tasks`, etc.) to locate the feature directory without relying on git branch name conventions.
|
|
106
106
|
|
|
107
|
+
- Also write / overwrite `.duaer/active-job.json` (job handoff pointer):
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"schemaVersion": 1,
|
|
111
|
+
"feature": "<directory-name only, e.g. 003-user-auth>",
|
|
112
|
+
"featureDirectory": "<resolved feature dir>",
|
|
113
|
+
"updatedAt": "<ISO-8601 UTC>",
|
|
114
|
+
"source": "duaer-specify"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
`feature` is the basename under `.duaer/specs/` (or `specs/`) that `duaer job` resolves.
|
|
118
|
+
|
|
107
119
|
**IMPORTANT**:
|
|
108
120
|
- You must only create one feature per `/duaer-specify` invocation
|
|
109
121
|
- The spec directory name and the git branch name are independent — they may be the same but that is the user's choice
|
|
110
122
|
- The spec directory and file are always created by this command, never by the hook
|
|
111
|
-
|
|
123
|
+
- Setting `active-job.json` makes this the job the employee must hand off; it does **not** lock git
|
|
112
124
|
4. Load the resolved active `spec-template` file to understand required sections.
|
|
113
125
|
|
|
114
126
|
5. **IF EXISTS**: Load `.duaer/memory/constitution.md` for project principles and governance constraints.
|
|
@@ -10,13 +10,14 @@ Duaer for what to build.
|
|
|
10
10
|
|
|
11
11
|
### I. Spec before code (NON-NEGOTIABLE)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Everyday: `/duaer-do` (assigns Spec, implements, converges).
|
|
14
|
+
Or the full chain. Never skip Spec or converge.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Default path for fine control:
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
`specify` → (optional plan/tasks) → `implement` → `converge` → `duaer status`
|
|
19
|
+
|
|
20
|
+
Hotfix may shorten; still Spec + converge (or `/duaer-do`).
|
|
20
21
|
|
|
21
22
|
### II. Read as-is before changing
|
|
22
23
|
|
|
@@ -36,7 +37,6 @@ Do not expand scope without updating the spec and tasks.
|
|
|
36
37
|
Mark tasks complete only when the stated verification (tests, manual checks,
|
|
37
38
|
or converge) has been performed or explicitly waived in the feature docs.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
machine-checkable.
|
|
40
|
+
**Job handoff (not a git lock):** prefer `duaer status`. Default policy is
|
|
41
|
+
`coach` — do not claim "done" while the active job is unfinished. Users should
|
|
42
|
+
not need to learn policy modes for everyday work.
|
package/ADOPT.md
CHANGED
|
@@ -1,77 +1,45 @@
|
|
|
1
1
|
# Adopt duaer-spec
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
delivery OS (method) and the agent-ops contract (how they are allowed to work).
|
|
3
|
+
Install Duaer so agents act like digital employees with a simple everyday loop.
|
|
5
4
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
From the target project (Node 18+):
|
|
5
|
+
## Install
|
|
9
6
|
|
|
10
7
|
```bash
|
|
11
8
|
npx duaer-spec init --here
|
|
12
|
-
|
|
13
|
-
# Pin a release
|
|
14
|
-
npx github:fujiezee/duaer-spec@v0.2.0 duaer init --here
|
|
15
|
-
|
|
16
|
-
# Lite (method only) / ops only / other integration branch
|
|
17
|
-
npx duaer-spec init --here --method
|
|
18
|
-
npx duaer-spec init --here --ops --branch develop
|
|
9
|
+
# pin: npx github:fujiezee/duaer-spec@v0.4.0 duaer init --here
|
|
19
10
|
```
|
|
20
11
|
|
|
21
|
-
From a clone of this repo:
|
|
22
|
-
|
|
23
12
|
```bash
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
npx duaer-spec init --here --method # lite
|
|
14
|
+
npx duaer-spec init --here --ops --branch develop
|
|
26
15
|
```
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Then:
|
|
17
|
+
## Everyday use
|
|
31
18
|
|
|
32
|
-
1.
|
|
33
|
-
2.
|
|
34
|
-
3. **Assign → work → accept → gate** — `/duaer-specify` → `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` → `/duaer-converge` → `duaer check . --gate`
|
|
19
|
+
1. `/duaer-do <ask>` in Cursor
|
|
20
|
+
2. `duaer status` when you want to see if the job is accepted
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
is not accepted. `duaer check --gate` is the machine merge check.
|
|
22
|
+
No need to learn policy modes or the full skill chain for normal work.
|
|
38
23
|
|
|
39
|
-
##
|
|
24
|
+
## Finer control (optional)
|
|
40
25
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cp docs/agent/workflow.md docs/agent/change-checklist.md docs/agent/e2e-test-plan.md \
|
|
49
|
-
/path/to/project/docs/agent/
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Duaer method
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
rsync -a .duaer/ /path/to/project/.duaer/
|
|
56
|
-
mkdir -p /path/to/project/.cursor/skills /path/to/project/.cursor/rules
|
|
57
|
-
rsync -a .cursor/skills/ /path/to/project/.cursor/skills/
|
|
58
|
-
cp .cursor/rules/duaer-spec.mdc /path/to/project/.cursor/rules/
|
|
59
|
-
cp DUADER.md /path/to/project/
|
|
60
|
-
```
|
|
26
|
+
| Command | When |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `/duaer-specify` | Only write / reshape the Brief |
|
|
29
|
+
| `/duaer-plan` · `/duaer-tasks` | Large or architectural jobs |
|
|
30
|
+
| `/duaer-implement` | Resume coding from `tasks.md` |
|
|
31
|
+
| `/duaer-converge` | Re-check Spec vs code |
|
|
32
|
+
| `duaer policy . strict` | Harder “don’t claim done” (still not a git lock) |
|
|
61
33
|
|
|
62
34
|
## Precedence
|
|
63
35
|
|
|
64
|
-
1. `AGENTS.md`
|
|
36
|
+
1. `AGENTS.md` — how employees operate
|
|
65
37
|
2. `DUADER.md` / `.duaer/` — what to build
|
|
66
|
-
3. Product overlays (do not copy `examples/` blindly)
|
|
67
38
|
|
|
68
39
|
## Updates
|
|
69
40
|
|
|
70
|
-
|
|
71
|
-
are not auto-updated.
|
|
41
|
+
`duaer init --force` (review the diff) or pin a tag. No auto-update.
|
|
72
42
|
|
|
73
43
|
## Do not copy
|
|
74
44
|
|
|
75
|
-
|
|
76
|
-
- `SOURCE.md` — provenance for maintainers
|
|
77
|
-
- `bin/` / `package.json` — only needed if you vendor the CLI itself
|
|
45
|
+
`examples/`, `SOURCE.md`, `bin/` / `package.json` (unless vendoring the CLI)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,54 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.4.0 — 2026-09-15
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Simple everyday ops
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- Constitution, DUADER, README, ADOPT, and merge checklist require the gate
|
|
12
|
-
- Honest scope: gate checks **handoff state**; Spec↔code judgment remains
|
|
13
|
-
agent-assisted
|
|
7
|
+
- **`/duaer-do`** — one Cursor skill for assign → work → accept
|
|
8
|
+
- **`duaer status`** — plain “accepted / not done” (alias of `job`)
|
|
9
|
+
- README / init / rules lead with hire → `/duaer-do` → `status`
|
|
10
|
+
- Step skills and policy remain available but are advanced, not the default story
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- E2E-003 / E2E-004 / E2E-005 for delivery report and gate
|
|
18
|
-
|
|
19
|
-
## 0.1.1 — 2026-09-15
|
|
12
|
+
## 0.3.0 — 2026-09-15
|
|
20
13
|
|
|
21
|
-
###
|
|
14
|
+
### Job handoff (not a repo lock)
|
|
22
15
|
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
16
|
+
- Default policy **`coach`**: guide the employee; do not claim "done" until the
|
|
17
|
+
**active job** is accepted — git merge is not blocked
|
|
18
|
+
- `.duaer/delivery-policy.json` modes: `off` | `coach` | `strict`
|
|
19
|
+
- `.duaer/active-job.json` set by `/duaer-specify`; `duaer job` reports status
|
|
20
|
+
- `duaer policy` to show/set mode
|
|
21
|
+
- `duaer check` defaults to active-job scope; `--strict` / policy `strict` optional
|
|
22
|
+
- `--gate` kept as deprecated alias for `--strict` (job handoff, not CI)
|
|
23
|
+
- `--all-jobs` optional; historical features no longer fail the default path
|
|
24
|
+
- Docs/rules/constitution demote merge-gate language
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
### E2E
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
- E2E-004–007 for coach default, strict optional, active-job scope
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
## 0.2.0 — 2026-09-15
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
- Home directory: `.duaer/` (not an embedded third-party kit)
|
|
37
|
-
- Cursor skills: `/duaer-specify`, `/duaer-plan`, `/duaer-tasks`, `/duaer-implement`, `/duaer-converge`, …
|
|
38
|
-
- Agent ops (`AGENTS.md`) remain authoritative over the method when they conflict
|
|
32
|
+
### Controllable delivery (machine gate)
|
|
39
33
|
|
|
40
|
-
|
|
34
|
+
- `/duaer-converge` writes `.duaer/specs/<feature>/delivery.json`
|
|
35
|
+
- `duaer check` / `--gate` for handoff state
|
|
36
|
+
- Honest scope: handoff state checkable; Spec↔code still agent-assisted
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
- `duaer check [dir]`
|
|
44
|
-
- `duaer version`
|
|
38
|
+
## 0.1.1 — 2026-09-15
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
- Digital-employee delivery OS positioning
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
# From GitHub (works now)
|
|
50
|
-
npx github:fujiezee/duaer-spec@v0.1.0 duaer init --here
|
|
42
|
+
## 0.1.0 — 2026-09-15
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
npx duaer-spec init --here
|
|
54
|
-
```
|
|
44
|
+
- First public methodology release
|
package/DUADER.md
CHANGED
|
@@ -1,40 +1,33 @@
|
|
|
1
1
|
# Duaer methodology
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
counts after **converge** accepts the result against that Brief.
|
|
3
|
+
Treat AI coding agents as **digital employees**. Everyday path is short; the
|
|
4
|
+
full chain exists when you need it.
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
**Precedence:** [`AGENTS.md`](AGENTS.md) wins on isolation, commits, Issue/PR.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
over anything here for isolation, commits, and Issue/PR gates.
|
|
8
|
+
## Everyday (default)
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
2. `.duaer/memory/project-context.md` — as-is implementation truth
|
|
16
|
-
3. `.duaer/memory/testing.md` — verification expectations
|
|
17
|
-
4. Active feature under `.duaer/specs/<nnn-slug>/` when one exists
|
|
10
|
+
```text
|
|
11
|
+
/duaer-do <ask> → duaer status
|
|
12
|
+
```
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
`/duaer-do` assigns a Brief, implements, converges, and reports handoff.
|
|
15
|
+
Do not require the user to run every phase skill.
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
## Full path (optional)
|
|
22
18
|
|
|
23
|
-
1. `/duaer-
|
|
24
|
-
2. `/duaer-
|
|
25
|
-
3. `/duaer-
|
|
26
|
-
4. `/duaer-
|
|
27
|
-
5. `/duaer-
|
|
28
|
-
6.
|
|
29
|
-
7. `/duaer-analyze` — optional consistency check
|
|
30
|
-
8. `/duaer-implement` — implement tasks only
|
|
31
|
-
9. `/duaer-converge` — **Accept**: compare result to Spec; write `delivery.json`; append remaining tasks if gaps
|
|
32
|
-
10. `duaer check . --gate` — **Gate**: fail merge if Spec missing, tasks open, or stamp not accepted
|
|
19
|
+
1. `/duaer-specify` — Brief
|
|
20
|
+
2. `/duaer-plan` — when architecture/contracts change
|
|
21
|
+
3. `/duaer-tasks` — explicit checkbox breakdown
|
|
22
|
+
4. `/duaer-implement` — code from tasks
|
|
23
|
+
5. `/duaer-converge` — accept stamp (`delivery.json`)
|
|
24
|
+
6. `duaer status` — may we claim this job done?
|
|
33
25
|
|
|
34
|
-
|
|
26
|
+
Hotfix: short Spec → tasks → implement → converge (or just `/duaer-do`).
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
Never skip a Brief or an accept step. Under default **coach** policy, do not
|
|
29
|
+
claim "done" while `duaer status` is unfinished. This is **job handoff**, not a
|
|
30
|
+
git merge lock.
|
|
38
31
|
|
|
39
32
|
## Install
|
|
40
33
|
|
|
@@ -42,18 +35,17 @@ Never skip specify (assign), converge (accept), or the gate.
|
|
|
42
35
|
npx duaer-spec init --here
|
|
43
36
|
```
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
installs this repository's method files into the target project.
|
|
38
|
+
See [`ADOPT.md`](ADOPT.md).
|
|
47
39
|
|
|
48
|
-
##
|
|
40
|
+
## Layout
|
|
49
41
|
|
|
50
42
|
```text
|
|
43
|
+
.duaer/delivery-policy.json # default coach — usually ignore
|
|
44
|
+
.duaer/active-job.json # current job
|
|
51
45
|
.duaer/specs/<nnn-slug>/
|
|
52
|
-
spec.md
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
delivery.json # after /duaer-converge (accepted | open)
|
|
56
|
-
… # research / contracts as needed
|
|
46
|
+
spec.md
|
|
47
|
+
tasks.md
|
|
48
|
+
delivery.json
|
|
57
49
|
```
|
|
58
50
|
|
|
59
51
|
## Related
|
package/README.md
CHANGED
|
@@ -1,102 +1,51 @@
|
|
|
1
1
|
# duaer-spec
|
|
2
2
|
|
|
3
|
-
**Duaer**
|
|
4
|
-
|
|
3
|
+
**Duaer** makes AI coding agents work like digital employees: assign a job,
|
|
4
|
+
get a controllable handoff — without a rigid git lock.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
Brief (Spec), follows a fixed work order, and only counts as done when the
|
|
8
|
-
**delivery gate** passes — not after a lucky chat.
|
|
9
|
-
|
|
10
|
-
Cursor is the labor. Duaer is hire → assign → accept.
|
|
11
|
-
|
|
12
|
-
## Install (onboard the employee)
|
|
6
|
+
## Everyday (this is enough)
|
|
13
7
|
|
|
14
8
|
```bash
|
|
15
9
|
npx duaer-spec init --here
|
|
16
|
-
# or pin: npx github:fujiezee/duaer-spec@v0.2.0 duaer init --here
|
|
17
10
|
```
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
# Lite: method only (personal / small changes)
|
|
21
|
-
npx duaer-spec init --here --method
|
|
12
|
+
In Cursor, one command:
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
```text
|
|
15
|
+
/duaer-do <what you want built or fixed>
|
|
25
16
|
```
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
Merge gate: `npx duaer-spec check . --gate`
|
|
29
|
-
Details: [`ADOPT.md`](ADOPT.md)
|
|
30
|
-
|
|
31
|
-
## How you run them
|
|
32
|
-
|
|
33
|
-
| Step | Meaning | Command |
|
|
34
|
-
|---|---|---|
|
|
35
|
-
| **Hire** | Install SOP + gates into the repo | `duaer init` |
|
|
36
|
-
| **Assign** | Write the Brief (what / why / acceptance) | `/duaer-specify` |
|
|
37
|
-
| **Work** | Plan, break down, implement | `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` |
|
|
38
|
-
| **Accept** | Converge stamps `delivery.json`; gaps stay open | `/duaer-converge` |
|
|
39
|
-
| **Gate** | Machine check before merge | `duaer check . --gate` |
|
|
40
|
-
|
|
41
|
-
Never skip **Assign** or **Accept**. Hotfix may shorten the middle; Spec,
|
|
42
|
-
converge, and the gate stay.
|
|
43
|
-
|
|
44
|
-
### What the gate actually enforces
|
|
45
|
-
|
|
46
|
-
`duaer check --gate` fails when any feature under `.duaer/specs/` has:
|
|
47
|
-
|
|
48
|
-
- missing `spec.md`
|
|
49
|
-
- open `- [ ]` tasks in `tasks.md`
|
|
50
|
-
- missing `delivery.json`, or `status` other than `accepted`
|
|
51
|
-
|
|
52
|
-
Converge judgment is still agent-assisted. The gate makes **handoff state**
|
|
53
|
-
checkable (and CI-friendly), not a substitute for tests.
|
|
18
|
+
See if the job can be reported done:
|
|
54
19
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|---|---|---|
|
|
59
|
-
| **Agent ops** | How the employee is allowed to operate | [`AGENTS.md`](AGENTS.md), [`docs/agent/`](docs/agent/) |
|
|
60
|
-
| **Duaer method** | What to build (Spec → Plan → Tasks → Implement → Converge) | [`DUADER.md`](DUADER.md), [`.duaer/`](.duaer/), [`.cursor/skills/`](.cursor/skills/) |
|
|
61
|
-
|
|
62
|
-
When they conflict, **agent ops win** — process beats improvisation.
|
|
20
|
+
```bash
|
|
21
|
+
duaer status
|
|
22
|
+
```
|
|
63
23
|
|
|
64
|
-
|
|
24
|
+
That’s the whole loop: **hire → do → status**.
|
|
65
25
|
|
|
66
|
-
|
|
67
|
-
constitution → specify → plan → tasks → implement → converge → check --gate
|
|
68
|
-
```
|
|
26
|
+
## What `/duaer-do` does
|
|
69
27
|
|
|
70
|
-
|
|
71
|
-
|
|
28
|
+
1. Writes a short Brief (Spec)
|
|
29
|
+
2. Implements it
|
|
30
|
+
3. Checks against the Brief and stamps accept / still-open
|
|
72
31
|
|
|
73
|
-
|
|
74
|
-
|
|
32
|
+
You do **not** need to chain specify → plan → tasks → implement → converge
|
|
33
|
+
for normal work. Those remain available when you want finer control.
|
|
75
34
|
|
|
76
|
-
##
|
|
35
|
+
## Optional
|
|
77
36
|
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
AGENTS.md Agent-ops contract (how employees operate)
|
|
82
|
-
DUADER.md Methodology conventions (how work is briefed)
|
|
83
|
-
ADOPT.md Onboarding guide
|
|
84
|
-
.duaer/ Memory, templates, workflows, scripts
|
|
85
|
-
.duaer/specs/<feature>/ Brief + tasks + delivery.json stamp
|
|
86
|
-
.cursor/rules/ Agent-ops + Duaer rules
|
|
87
|
-
.cursor/skills/ duaer-* skills
|
|
88
|
-
docs/agent/ Workflow detail + checklists
|
|
89
|
-
docs/maintaining.md How maintainers evolve the method
|
|
90
|
-
docs/npm-trusted-publishing.md npm Trusted Publishing (OIDC) setup
|
|
91
|
-
examples/ Optional product overlays (not defaults)
|
|
37
|
+
```bash
|
|
38
|
+
duaer status # active job: accepted or not
|
|
39
|
+
npx duaer-spec@0.4.0 init --here --method # method only
|
|
92
40
|
```
|
|
93
41
|
|
|
94
|
-
|
|
42
|
+
Advanced (policy, full check, step-by-step skills): [`ADOPT.md`](ADOPT.md) · [`DUADER.md`](DUADER.md)
|
|
95
43
|
|
|
96
|
-
|
|
97
|
-
- One request → one branch + one worktree → merge → delete worktree
|
|
98
|
-
- Push only when explicitly requested
|
|
44
|
+
## Two layers
|
|
99
45
|
|
|
100
|
-
|
|
46
|
+
| Layer | Role |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **Agent ops** | How agents may operate — [`AGENTS.md`](AGENTS.md) |
|
|
49
|
+
| **Duaer method** | How jobs are briefed and accepted — [`.duaer/`](.duaer/), skills |
|
|
101
50
|
|
|
102
|
-
|
|
51
|
+
When they conflict, **agent ops win**.
|
package/bin/duaer.mjs
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* duaer — CLI for the duaer-spec delivery OS (digital employees)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* duaer version
|
|
8
|
-
* duaer help
|
|
5
|
+
* Everyday: duaer init | duaer status
|
|
6
|
+
* Advanced: check | job | policy | version
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import {
|
|
@@ -24,35 +22,27 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
24
22
|
const PKG_ROOT = resolve(__dirname, '..')
|
|
25
23
|
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
26
24
|
|
|
27
|
-
const
|
|
25
|
+
const POLICY_MODES = new Set(['off', 'coach', 'strict'])
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
const USAGE = `duaer — digital-employee delivery (duaer-spec ${PKG.version})
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
duaer init [
|
|
33
|
-
duaer
|
|
34
|
-
duaer version Print version
|
|
35
|
-
duaer help Show this help
|
|
29
|
+
Everyday:
|
|
30
|
+
duaer init [--here] Hire into a project
|
|
31
|
+
duaer status [dir] Is the active job accepted?
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
--all Full hire: agent ops + method (default)
|
|
39
|
-
--method Lite: method only (.duaer, skills, DUADER.md, duaer-spec rule)
|
|
40
|
-
--ops Ops only (AGENTS.md, docs/agent, ops rules)
|
|
41
|
-
--force Overwrite existing managed files
|
|
42
|
-
--branch <n> Integration branch for baseline note (default: main)
|
|
43
|
-
--here Same as dir=.
|
|
33
|
+
In Cursor: /duaer-do <what you want>
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
Advanced:
|
|
36
|
+
duaer check [dir] [--workplace|--job|--all-jobs|--strict]
|
|
37
|
+
duaer job [dir] (same as status)
|
|
38
|
+
duaer policy [dir] [off|coach|strict]
|
|
39
|
+
duaer version
|
|
40
|
+
|
|
41
|
+
Init: --all (default) | --method | --ops | --force | --branch <n> | --here
|
|
51
42
|
|
|
52
43
|
Examples:
|
|
53
44
|
npx duaer-spec init --here
|
|
54
|
-
duaer
|
|
55
|
-
duaer check . --gate
|
|
45
|
+
duaer status
|
|
56
46
|
`
|
|
57
47
|
|
|
58
48
|
function parseArgs(argv) {
|
|
@@ -65,7 +55,10 @@ function parseArgs(argv) {
|
|
|
65
55
|
branch: 'main',
|
|
66
56
|
workplace: false,
|
|
67
57
|
delivery: false,
|
|
68
|
-
|
|
58
|
+
job: false,
|
|
59
|
+
allJobs: false,
|
|
60
|
+
strict: false,
|
|
61
|
+
policyMode: null,
|
|
69
62
|
}
|
|
70
63
|
const rest = args.slice(1)
|
|
71
64
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -77,12 +70,18 @@ function parseArgs(argv) {
|
|
|
77
70
|
else if (a === '--here') out.dir = '.'
|
|
78
71
|
else if (a === '--workplace') out.workplace = true
|
|
79
72
|
else if (a === '--delivery') out.delivery = true
|
|
80
|
-
else if (a === '--
|
|
73
|
+
else if (a === '--job') out.job = true
|
|
74
|
+
else if (a === '--all-jobs') out.allJobs = true
|
|
75
|
+
else if (a === '--strict' || a === '--gate') out.strict = true
|
|
81
76
|
else if (a === '--branch') {
|
|
82
77
|
out.branch = rest[++i]
|
|
83
78
|
if (!out.branch) throw new Error('--branch requires a value')
|
|
79
|
+
} else if (POLICY_MODES.has(a) && out.cmd === 'policy') {
|
|
80
|
+
out.policyMode = a
|
|
84
81
|
} else if (a.startsWith('-')) {
|
|
85
82
|
throw new Error(`Unknown flag: ${a}`)
|
|
83
|
+
} else if (out.cmd === 'policy' && POLICY_MODES.has(a)) {
|
|
84
|
+
out.policyMode = a
|
|
86
85
|
} else {
|
|
87
86
|
out.dir = a
|
|
88
87
|
}
|
|
@@ -124,10 +123,62 @@ function writeIfNeeded(path, content, { force }) {
|
|
|
124
123
|
console.log(`wrote ${path}`)
|
|
125
124
|
}
|
|
126
125
|
|
|
126
|
+
function readJson(path, fallback = null) {
|
|
127
|
+
if (!existsSync(path)) return fallback
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
130
|
+
} catch {
|
|
131
|
+
return fallback
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function defaultPolicy() {
|
|
136
|
+
return {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
mode: 'coach',
|
|
139
|
+
scope: 'active',
|
|
140
|
+
notes:
|
|
141
|
+
'Job-level handoff. off=record only; coach=guide (default); strict=do not claim done until accepted. Not a git merge lock.',
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function policyPath(target) {
|
|
146
|
+
return join(target, '.duaer', 'delivery-policy.json')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function activeJobPath(target) {
|
|
150
|
+
return join(target, '.duaer', 'active-job.json')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function loadPolicy(target) {
|
|
154
|
+
const p = readJson(policyPath(target), null)
|
|
155
|
+
if (!p) return defaultPolicy()
|
|
156
|
+
const mode = POLICY_MODES.has(p.mode) ? p.mode : 'coach'
|
|
157
|
+
return { ...defaultPolicy(), ...p, mode }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writePolicy(target, mode) {
|
|
161
|
+
const next = { ...defaultPolicy(), mode, updatedAt: new Date().toISOString() }
|
|
162
|
+
ensureDir(join(target, '.duaer'))
|
|
163
|
+
writeFileSync(policyPath(target), JSON.stringify(next, null, 2) + '\n')
|
|
164
|
+
return next
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writeDefaultPolicy(target, { force }) {
|
|
168
|
+
const path = policyPath(target)
|
|
169
|
+
if (existsSync(path) && !force) {
|
|
170
|
+
console.log(`skip (exists): ${path}`)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
writeFileSync(path, JSON.stringify(defaultPolicy(), null, 2) + '\n')
|
|
174
|
+
console.log(' .duaer/delivery-policy.json (mode=coach)')
|
|
175
|
+
}
|
|
176
|
+
|
|
127
177
|
function installMethod(target, opts) {
|
|
128
178
|
console.log('Installing Duaer method…')
|
|
129
179
|
copyPath(join(PKG_ROOT, '.duaer'), join(target, '.duaer'), opts)
|
|
130
180
|
console.log(' .duaer/')
|
|
181
|
+
writeDefaultPolicy(target, opts)
|
|
131
182
|
|
|
132
183
|
ensureDir(join(target, '.cursor', 'skills'))
|
|
133
184
|
const skillsRoot = join(PKG_ROOT, '.cursor', 'skills')
|
|
@@ -232,19 +283,12 @@ function cmdInit(opts) {
|
|
|
232
283
|
}
|
|
233
284
|
|
|
234
285
|
console.log(`
|
|
235
|
-
Hired.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
4. Work — /duaer-plan → /duaer-tasks → /duaer-implement
|
|
242
|
-
5. Accept — /duaer-converge (writes delivery.json; gaps stay open)
|
|
243
|
-
6. Gate — duaer check . --gate before merge
|
|
244
|
-
7. Policy — AGENTS.md wins over DUADER.md when they conflict
|
|
245
|
-
|
|
246
|
-
No Spec = not assigned. Open tasks or open delivery.json = not accepted.
|
|
247
|
-
See DUADER.md and AGENTS.md in the target project.
|
|
286
|
+
Hired. Everyday loop:
|
|
287
|
+
|
|
288
|
+
1. In Cursor: /duaer-do <what you want>
|
|
289
|
+
2. Optional: duaer status
|
|
290
|
+
|
|
291
|
+
That is enough for normal work. Advanced: ADOPT.md / DUADER.md
|
|
248
292
|
`)
|
|
249
293
|
}
|
|
250
294
|
|
|
@@ -265,6 +309,55 @@ function readDelivery(path) {
|
|
|
265
309
|
}
|
|
266
310
|
}
|
|
267
311
|
|
|
312
|
+
function featureVerdict(name, dir) {
|
|
313
|
+
const specPath = join(dir, 'spec.md')
|
|
314
|
+
const tasksPath = join(dir, 'tasks.md')
|
|
315
|
+
const deliveryPath = join(dir, 'delivery.json')
|
|
316
|
+
const tasks = countOpenTasks(tasksPath)
|
|
317
|
+
const delivery = readDelivery(deliveryPath)
|
|
318
|
+
const hasSpec = existsSync(specPath)
|
|
319
|
+
const stamp = delivery?.status || 'none'
|
|
320
|
+
let verdict = 'accepted'
|
|
321
|
+
const blockers = []
|
|
322
|
+
|
|
323
|
+
if (!hasSpec) {
|
|
324
|
+
verdict = 'blocked'
|
|
325
|
+
blockers.push('missing spec.md')
|
|
326
|
+
}
|
|
327
|
+
if (tasks && tasks.open > 0) {
|
|
328
|
+
verdict = 'blocked'
|
|
329
|
+
blockers.push(`${tasks.open} open task(s)`)
|
|
330
|
+
}
|
|
331
|
+
if (stamp === 'open' || stamp === 'invalid') {
|
|
332
|
+
verdict = 'blocked'
|
|
333
|
+
blockers.push(`delivery.json status=${stamp}`)
|
|
334
|
+
}
|
|
335
|
+
if (hasSpec && tasks && tasks.open === 0 && (stamp === 'none' || !delivery)) {
|
|
336
|
+
verdict = 'unaccepted'
|
|
337
|
+
blockers.push('no delivery.json (run /duaer-converge)')
|
|
338
|
+
}
|
|
339
|
+
if (hasSpec && !tasks && (stamp === 'none' || !delivery)) {
|
|
340
|
+
verdict = 'unaccepted'
|
|
341
|
+
blockers.push('no tasks.md / no delivery.json')
|
|
342
|
+
}
|
|
343
|
+
if (stamp === 'accepted' && tasks && tasks.open === 0 && hasSpec) {
|
|
344
|
+
verdict = 'accepted'
|
|
345
|
+
blockers.length = 0
|
|
346
|
+
} else if (stamp === 'accepted' && (!tasks || tasks.open === 0) && hasSpec && !tasks) {
|
|
347
|
+
// accepted stamp with no tasks file — treat as accepted only if stamp says so
|
|
348
|
+
// keep unaccepted path above for !tasks && no stamp
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Stamp accepted but open tasks remain → blocked (fake stamp)
|
|
352
|
+
if (stamp === 'accepted' && tasks && tasks.open > 0) {
|
|
353
|
+
verdict = 'blocked'
|
|
354
|
+
blockers.length = 0
|
|
355
|
+
blockers.push('delivery.json accepted but open tasks remain')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return { name, hasSpec, tasks, stamp, verdict, blockers }
|
|
359
|
+
}
|
|
360
|
+
|
|
268
361
|
function listFeatures(target) {
|
|
269
362
|
const root = join(target, '.duaer', 'specs')
|
|
270
363
|
if (!existsSync(root)) return []
|
|
@@ -277,53 +370,28 @@ function listFeatures(target) {
|
|
|
277
370
|
}
|
|
278
371
|
})
|
|
279
372
|
.sort()
|
|
280
|
-
.map((name) =>
|
|
281
|
-
|
|
282
|
-
const specPath = join(dir, 'spec.md')
|
|
283
|
-
const tasksPath = join(dir, 'tasks.md')
|
|
284
|
-
const deliveryPath = join(dir, 'delivery.json')
|
|
285
|
-
const tasks = countOpenTasks(tasksPath)
|
|
286
|
-
const delivery = readDelivery(deliveryPath)
|
|
287
|
-
const hasSpec = existsSync(specPath)
|
|
288
|
-
const stamp = delivery?.status || 'none'
|
|
289
|
-
let verdict = 'accepted'
|
|
290
|
-
const blockers = []
|
|
291
|
-
|
|
292
|
-
if (!hasSpec) {
|
|
293
|
-
verdict = 'blocked'
|
|
294
|
-
blockers.push('missing spec.md')
|
|
295
|
-
}
|
|
296
|
-
if (tasks && tasks.open > 0) {
|
|
297
|
-
verdict = 'blocked'
|
|
298
|
-
blockers.push(`${tasks.open} open task(s)`)
|
|
299
|
-
}
|
|
300
|
-
if (stamp === 'open' || stamp === 'invalid') {
|
|
301
|
-
verdict = 'blocked'
|
|
302
|
-
blockers.push(`delivery.json status=${stamp}`)
|
|
303
|
-
}
|
|
304
|
-
if (hasSpec && tasks && tasks.open === 0 && (stamp === 'none' || !delivery)) {
|
|
305
|
-
// Brief exists and tasks clear, but never stamped by converge
|
|
306
|
-
verdict = 'unaccepted'
|
|
307
|
-
blockers.push('no delivery.json (run /duaer-converge)')
|
|
308
|
-
}
|
|
309
|
-
if (hasSpec && !tasks && (stamp === 'none' || !delivery)) {
|
|
310
|
-
verdict = 'unaccepted'
|
|
311
|
-
blockers.push('no tasks.md / no delivery.json')
|
|
312
|
-
}
|
|
313
|
-
if (stamp === 'accepted' && tasks && tasks.open === 0 && hasSpec) {
|
|
314
|
-
verdict = 'accepted'
|
|
315
|
-
blockers.length = 0
|
|
316
|
-
}
|
|
373
|
+
.map((name) => featureVerdict(name, join(root, name)))
|
|
374
|
+
}
|
|
317
375
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
376
|
+
function resolveActiveJobName(target, features) {
|
|
377
|
+
const marker = readJson(activeJobPath(target), null)
|
|
378
|
+
if (marker?.feature && features.some((f) => f.name === marker.feature)) {
|
|
379
|
+
return marker.feature
|
|
380
|
+
}
|
|
381
|
+
if (features.length === 0) return null
|
|
382
|
+
// Prefer newest directory name (lexicographic works for NNN-slug)
|
|
383
|
+
return features[features.length - 1].name
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function printFeature(f, { label } = {}) {
|
|
387
|
+
const taskInfo = f.tasks
|
|
388
|
+
? `tasks open=${f.tasks.open} done=${f.tasks.done}`
|
|
389
|
+
: 'no tasks.md'
|
|
390
|
+
const prefix = label ? `${label} ` : ''
|
|
391
|
+
console.log(
|
|
392
|
+
`${prefix}${f.verdict.padEnd(10)} ${f.name} spec=${f.hasSpec ? 'yes' : 'NO'} stamp=${f.stamp} ${taskInfo}`,
|
|
393
|
+
)
|
|
394
|
+
for (const b of f.blockers) console.log(` · ${b}`)
|
|
327
395
|
}
|
|
328
396
|
|
|
329
397
|
function checkWorkplace(target) {
|
|
@@ -346,51 +414,91 @@ function checkWorkplace(target) {
|
|
|
346
414
|
return missing
|
|
347
415
|
}
|
|
348
416
|
|
|
349
|
-
function checkDelivery(target, {
|
|
417
|
+
function checkDelivery(target, { allJobs, policy }) {
|
|
350
418
|
const features = listFeatures(target)
|
|
351
|
-
|
|
419
|
+
const activeName = resolveActiveJobName(target, features)
|
|
420
|
+
const active = features.find((f) => f.name === activeName) || null
|
|
421
|
+
|
|
422
|
+
console.log('\n## Handoff (jobs)\n')
|
|
423
|
+
console.log(`policy: ${policy.mode} scope: ${allJobs ? 'all' : 'active'}`)
|
|
424
|
+
|
|
352
425
|
if (features.length === 0) {
|
|
353
|
-
console.log('ok no features under .duaer/specs/ (nothing to
|
|
354
|
-
return { blocked: 0, unaccepted: 0, features: 0 }
|
|
426
|
+
console.log('ok no features under .duaer/specs/ (nothing to hand off)')
|
|
427
|
+
return { blocked: 0, unaccepted: 0, features: 0, active: null, unfinished: false }
|
|
355
428
|
}
|
|
356
429
|
|
|
430
|
+
if (allJobs) {
|
|
431
|
+
for (const f of features) {
|
|
432
|
+
printFeature(f, { label: f.name === activeName ? '★' : ' ' })
|
|
433
|
+
}
|
|
434
|
+
} else if (active) {
|
|
435
|
+
printFeature(active, { label: '★' })
|
|
436
|
+
console.log(`\nactive job: ${active.name} (from .duaer/active-job.json or latest spec)`)
|
|
437
|
+
} else {
|
|
438
|
+
console.log('ok no resolvable active job')
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const focus = allJobs ? features : active ? [active] : []
|
|
357
442
|
let blocked = 0
|
|
358
443
|
let unaccepted = 0
|
|
359
|
-
for (const f of
|
|
360
|
-
const taskInfo = f.tasks
|
|
361
|
-
? `tasks open=${f.tasks.open} done=${f.tasks.done}`
|
|
362
|
-
: 'no tasks.md'
|
|
363
|
-
const line = `${f.verdict.padEnd(10)} ${f.name} spec=${f.hasSpec ? 'yes' : 'NO'} stamp=${f.stamp} ${taskInfo}`
|
|
364
|
-
console.log(line)
|
|
365
|
-
if (f.blockers.length) {
|
|
366
|
-
for (const b of f.blockers) console.log(` · ${b}`)
|
|
367
|
-
}
|
|
444
|
+
for (const f of focus) {
|
|
368
445
|
if (f.verdict === 'blocked') blocked++
|
|
369
446
|
if (f.verdict === 'unaccepted') unaccepted++
|
|
370
447
|
}
|
|
448
|
+
const unfinished = blocked + unaccepted > 0
|
|
449
|
+
return { blocked, unaccepted, features: focus.length, active, unfinished }
|
|
450
|
+
}
|
|
371
451
|
|
|
372
|
-
|
|
373
|
-
|
|
452
|
+
function handoffAdvice(policy, unfinished, strictFlag) {
|
|
453
|
+
if (!unfinished) {
|
|
454
|
+
console.log('\nActive job accepted — employee may report delivery complete.')
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
if (policy.mode === 'off' && !strictFlag) {
|
|
458
|
+
console.log('\nHandoff incomplete (policy=off) — recorded only.')
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
if (policy.mode === 'coach' && !strictFlag) {
|
|
462
|
+
console.log(`
|
|
463
|
+
Coach: this job is not accepted yet.
|
|
464
|
+
→ close open tasks, run /duaer-converge, then duaer job .
|
|
465
|
+
→ do not tell the user the work is "done" until status=accepted
|
|
466
|
+
→ git is not blocked; this is job etiquette, not a merge lock
|
|
467
|
+
`)
|
|
468
|
+
return
|
|
374
469
|
}
|
|
375
|
-
|
|
470
|
+
console.log(`
|
|
471
|
+
Strict: active job not accepted — exit 1.
|
|
472
|
+
→ /duaer-converge until delivery.json status=accepted and tasks clear
|
|
473
|
+
`)
|
|
376
474
|
}
|
|
377
475
|
|
|
378
476
|
function cmdCheck(opts) {
|
|
379
477
|
const target = resolve(opts.dir)
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
478
|
+
const policy = loadPolicy(target)
|
|
479
|
+
const strictFlag = opts.strict || policy.mode === 'strict'
|
|
480
|
+
|
|
481
|
+
// default: workplace + delivery; --workplace alone; job/delivery/strict/all-jobs → delivery
|
|
482
|
+
let wp = false
|
|
483
|
+
let del = false
|
|
484
|
+
if (opts.workplace && !opts.delivery && !opts.job && !opts.allJobs && !opts.strict) {
|
|
485
|
+
wp = true
|
|
486
|
+
} else if (opts.delivery || opts.job || opts.allJobs || opts.strict) {
|
|
487
|
+
del = true
|
|
488
|
+
if (opts.workplace) wp = true
|
|
489
|
+
} else {
|
|
490
|
+
wp = true
|
|
491
|
+
del = true
|
|
492
|
+
}
|
|
383
493
|
|
|
384
|
-
console.log(`Checking ${target}${
|
|
494
|
+
console.log(`Checking ${target}${strictFlag ? ' (strict handoff)' : ''}\n`)
|
|
385
495
|
|
|
386
496
|
let workplaceMissing = 0
|
|
387
|
-
if (
|
|
388
|
-
workplaceMissing = checkWorkplace(target)
|
|
389
|
-
}
|
|
497
|
+
if (wp) workplaceMissing = checkWorkplace(target)
|
|
390
498
|
|
|
391
|
-
let delivery = {
|
|
392
|
-
if (
|
|
393
|
-
delivery = checkDelivery(target, {
|
|
499
|
+
let delivery = { unfinished: false }
|
|
500
|
+
if (del) {
|
|
501
|
+
delivery = checkDelivery(target, { allJobs: opts.allJobs, policy })
|
|
394
502
|
}
|
|
395
503
|
|
|
396
504
|
let fail = false
|
|
@@ -399,36 +507,77 @@ function cmdCheck(opts) {
|
|
|
399
507
|
fail = true
|
|
400
508
|
}
|
|
401
509
|
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
510
|
+
if (del) {
|
|
511
|
+
handoffAdvice(policy, delivery.unfinished, opts.strict)
|
|
512
|
+
if (delivery.unfinished && strictFlag) {
|
|
405
513
|
fail = true
|
|
406
514
|
}
|
|
407
|
-
if (opts.gate && delivery.unaccepted) {
|
|
408
|
-
console.log(
|
|
409
|
-
`\n${delivery.unaccepted} feature(s) not accepted — run /duaer-converge until delivery.json status=accepted`,
|
|
410
|
-
)
|
|
411
|
-
fail = true
|
|
412
|
-
} else if (!opts.gate && delivery.unaccepted) {
|
|
413
|
-
console.log(
|
|
414
|
-
`\n${delivery.unaccepted} feature(s) not yet accepted (warning). Use --gate to fail the merge check.`,
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
515
|
}
|
|
418
516
|
|
|
419
517
|
if (!fail) {
|
|
420
|
-
if (
|
|
421
|
-
console.log('\
|
|
422
|
-
} else if (
|
|
423
|
-
console.log('\
|
|
518
|
+
if (wp && !workplaceMissing && !del) {
|
|
519
|
+
console.log('\nWorkplace ready.')
|
|
520
|
+
} else if (!delivery.unfinished) {
|
|
521
|
+
console.log('\nCheck ok.')
|
|
424
522
|
} else {
|
|
425
|
-
console.log('\
|
|
523
|
+
console.log('\nCheck ok (handoff still open under coach/off — see above).')
|
|
426
524
|
}
|
|
427
525
|
} else {
|
|
428
526
|
process.exitCode = 1
|
|
429
527
|
}
|
|
430
528
|
}
|
|
431
529
|
|
|
530
|
+
function cmdJob(opts) {
|
|
531
|
+
const target = resolve(opts.dir)
|
|
532
|
+
const policy = loadPolicy(target)
|
|
533
|
+
const features = listFeatures(target)
|
|
534
|
+
const activeName = resolveActiveJobName(target, features)
|
|
535
|
+
const active = features.find((f) => f.name === activeName) || null
|
|
536
|
+
|
|
537
|
+
console.log(`Status @ ${target}`)
|
|
538
|
+
if (!active) {
|
|
539
|
+
console.log('No active job yet. In Cursor run: /duaer-do <what you want>')
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
if (active.verdict === 'accepted') {
|
|
543
|
+
console.log(`✅ accepted ${active.name}`)
|
|
544
|
+
console.log('Ready for your review.')
|
|
545
|
+
} else {
|
|
546
|
+
console.log(`⏳ not done ${active.name}`)
|
|
547
|
+
for (const b of active.blockers) console.log(` · ${b}`)
|
|
548
|
+
console.log('Next: /duaer-do (resume) or /duaer-converge')
|
|
549
|
+
}
|
|
550
|
+
if (policy.mode === 'strict' && active.verdict !== 'accepted') {
|
|
551
|
+
console.log('(policy=strict — do not claim done yet)')
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function cmdPolicy(opts) {
|
|
556
|
+
const target = resolve(opts.dir)
|
|
557
|
+
if (opts.policyMode) {
|
|
558
|
+
if (!POLICY_MODES.has(opts.policyMode)) {
|
|
559
|
+
throw new Error(`Unknown policy mode: ${opts.policyMode}`)
|
|
560
|
+
}
|
|
561
|
+
const next = writePolicy(target, opts.policyMode)
|
|
562
|
+
console.log(`Wrote ${policyPath(target)}`)
|
|
563
|
+
console.log(`mode: ${next.mode}`)
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
const policy = loadPolicy(target)
|
|
567
|
+
const exists = existsSync(policyPath(target))
|
|
568
|
+
console.log(`Policy @ ${target}`)
|
|
569
|
+
console.log(`file: ${exists ? policyPath(target) : '(defaults; run init or: duaer policy . coach)'}`)
|
|
570
|
+
console.log(`mode: ${policy.mode}`)
|
|
571
|
+
console.log(`scope: ${policy.scope || 'active'}`)
|
|
572
|
+
console.log(`
|
|
573
|
+
Modes:
|
|
574
|
+
off record only
|
|
575
|
+
coach guide; do not claim done until accepted (default)
|
|
576
|
+
strict check fails + agents must not report done until accepted
|
|
577
|
+
Not a git merge lock.
|
|
578
|
+
`)
|
|
579
|
+
}
|
|
580
|
+
|
|
432
581
|
function main() {
|
|
433
582
|
let opts
|
|
434
583
|
try {
|
|
@@ -447,6 +596,13 @@ function main() {
|
|
|
447
596
|
case 'check':
|
|
448
597
|
cmdCheck(opts)
|
|
449
598
|
break
|
|
599
|
+
case 'job':
|
|
600
|
+
case 'status':
|
|
601
|
+
cmdJob(opts)
|
|
602
|
+
break
|
|
603
|
+
case 'policy':
|
|
604
|
+
cmdPolicy(opts)
|
|
605
|
+
break
|
|
450
606
|
case 'version':
|
|
451
607
|
case '--version':
|
|
452
608
|
case '-v':
|
|
@@ -94,7 +94,7 @@ See [spec update guidance](workflow.md#3-spec-update-guidance).
|
|
|
94
94
|
|
|
95
95
|
## 6. Merge / PR
|
|
96
96
|
|
|
97
|
-
- [ ]
|
|
97
|
+
- [ ] Active job handoff considered: `duaer job .` (coach/strict: do not claim done if unfinished).
|
|
98
98
|
- [ ] Branch refreshed against latest `main`.
|
|
99
99
|
- [ ] Merged into local `main` (and/or remote PR/MR when required).
|
|
100
100
|
- [ ] Only this request's logical changes included.
|
|
@@ -122,7 +122,7 @@ See [spec update guidance](workflow.md#3-spec-update-guidance).
|
|
|
122
122
|
| 3 | Impacted specs updated | [R1](workflow.md#r1--spec-first--spec-sync) |
|
|
123
123
|
| 4 | E2E docs updated or confirmed unnecessary | [R3](workflow.md#r3--e2e-coverage-doc) |
|
|
124
124
|
| 5 | Targeted validation done or waived with reason | Development loop |
|
|
125
|
-
| 5a |
|
|
125
|
+
| 5a | Active job handoff via `duaer job` (coach/strict) | Delivery OS — not a git lock |
|
|
126
126
|
| 6 | Conventional commits | [R2](workflow.md#r2--commit-per-change) |
|
|
127
127
|
| 7 | No secrets or local data | [§4](workflow.md#4-what-never-to-commit) |
|
|
128
128
|
| 8 | Merged into `main`; worktree and branch removed | [R4](workflow.md#r4--request-branch--worktree--merge-gate) · [§6.1](#61-merge-cleanup) |
|
|
@@ -25,11 +25,14 @@ the table for their app.
|
|
|
25
25
|
|
|
26
26
|
| ID | Title | Status |
|
|
27
27
|
|---|---|---|
|
|
28
|
-
| E2E-001 | After `duaer init`, CLI prints
|
|
29
|
-
| E2E-002 | README
|
|
30
|
-
| E2E-003 | `duaer
|
|
31
|
-
| E2E-004 | `
|
|
32
|
-
| E2E-005 | After converge stamps `accepted`,
|
|
28
|
+
| E2E-001 | After `duaer init`, CLI prints simple `/duaer-do` next steps | manual |
|
|
29
|
+
| E2E-002 | README everyday path is hire → `/duaer-do` → `status` | manual |
|
|
30
|
+
| E2E-003 | `duaer status` shows accepted / not done in plain language | manual |
|
|
31
|
+
| E2E-004 | Default `coach` policy: unfinished job warns but check exits 0 | manual |
|
|
32
|
+
| E2E-005 | After converge stamps `accepted`, `duaer status` shows accepted | manual |
|
|
33
|
+
| E2E-006 | `duaer policy . strict` then unfinished job makes check exit 1 | manual |
|
|
34
|
+
| E2E-007 | `--all-jobs` is optional; default scope is active job only | manual |
|
|
35
|
+
| E2E-008 | `/duaer-do` skill is installed by `duaer init` | manual |
|
|
33
36
|
|
|
34
37
|
## Traceability
|
|
35
38
|
|
|
@@ -37,8 +40,11 @@ Keep this matrix current when scenarios or specs change.
|
|
|
37
40
|
|
|
38
41
|
| Scenario | Spec / feature | Notes |
|
|
39
42
|
|---|---|---|
|
|
40
|
-
| E2E-001 | `bin/duaer.mjs` init banner |
|
|
41
|
-
| E2E-002 | `README.md`
|
|
42
|
-
| E2E-003 | `
|
|
43
|
-
| E2E-004 | `
|
|
44
|
-
| E2E-005 |
|
|
43
|
+
| E2E-001 | `bin/duaer.mjs` init banner | Simple onboarding |
|
|
44
|
+
| E2E-002 | `README.md` | Everyday path |
|
|
45
|
+
| E2E-003 | `duaer status` | Plain language |
|
|
46
|
+
| E2E-004 | `delivery-policy.json` coach | Not a repo lock |
|
|
47
|
+
| E2E-005 | converge → `delivery.json` | Accept stamp |
|
|
48
|
+
| E2E-006 | policy strict | Optional |
|
|
49
|
+
| E2E-007 | `--all-jobs` vs active | Scope |
|
|
50
|
+
| E2E-008 | `.cursor/skills/duaer-do` | One-shot skill |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "duaer-spec",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Simple digital-employee delivery: /duaer-do + duaer status",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"duaer": "bin/duaer.mjs",
|