duaer-spec 0.1.1 → 0.3.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 +22 -5
- package/.cursor/skills/duaer-converge/SKILL.md +49 -7
- package/.cursor/skills/duaer-specify/SKILL.md +13 -1
- package/.duaer/delivery-policy.json +6 -0
- package/.duaer/memory/constitution.md +11 -0
- package/ADOPT.md +9 -7
- package/CHANGELOG.md +34 -3
- package/DUADER.md +21 -14
- package/README.md +36 -39
- package/bin/duaer.mjs +378 -25
- package/docs/agent/change-checklist.md +2 -0
- package/docs/agent/e2e-test-plan.md +11 -1
- package/package.json +2 -2
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Features and hotfixes must go through Duaer;
|
|
2
|
+
description: Features and hotfixes must go through Duaer; job handoff before claiming done
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Duaer required (NON-NEGOTIABLE)
|
|
7
7
|
|
|
8
|
-
Treat agents as digital employees
|
|
9
|
-
only
|
|
8
|
+
Treat agents as **digital employees**: assign a Brief (Spec) before code; accept
|
|
9
|
+
via converge; only then claim the **job** is done. See `DUADER.md` and
|
|
10
|
+
`.duaer/memory/constitution.md`.
|
|
10
11
|
|
|
11
12
|
If this rule conflicts with root `AGENTS.md` / `docs/agent/` (worktrees, commits,
|
|
12
13
|
Issue/PR), **AGENTS.md wins**.
|
|
13
14
|
|
|
15
|
+
## Handoff policy (job-level, not a repo lock)
|
|
16
|
+
|
|
17
|
+
Read `.duaer/delivery-policy.json` (`off` | `coach` | `strict`, default **coach**).
|
|
18
|
+
|
|
19
|
+
- This controls whether you may **report the active job complete** — not whether
|
|
20
|
+
git may commit or merge.
|
|
21
|
+
- Active job: `.duaer/active-job.json` → `feature`, else the latest `.duaer/specs/*`.
|
|
22
|
+
- Check with `duaer job .` before telling the user work is finished.
|
|
23
|
+
|
|
24
|
+
| Mode | Agent behavior when active job is not accepted |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `off` | May continue; do not invent an accept stamp |
|
|
27
|
+
| `coach` | Warn; guide to specify / tasks / converge; **do not say "done"** |
|
|
28
|
+
| `strict` | **Refuse** to report delivery complete until `duaer job` shows accepted |
|
|
29
|
+
|
|
14
30
|
## When Duaer is mandatory
|
|
15
31
|
|
|
16
32
|
Always write/update a feature spec (Brief) before coding for:
|
|
@@ -23,7 +39,7 @@ Forbidden: jump from chat to code with no `spec.md` / feature directory under `.
|
|
|
23
39
|
|
|
24
40
|
## Default flow
|
|
25
41
|
|
|
26
|
-
`/duaer-specify` → `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` → `/duaer-converge`
|
|
42
|
+
`/duaer-specify` → `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` → `/duaer-converge` → `duaer job`
|
|
27
43
|
|
|
28
44
|
## Hotfix (still Spec, shortened)
|
|
29
45
|
|
|
@@ -31,4 +47,5 @@ Forbidden: jump from chat to code with no `spec.md` / feature directory under `.
|
|
|
31
47
|
|
|
32
48
|
- Implement only from `tasks.md` when it exists
|
|
33
49
|
- Update `.duaer/memory/project-context.md` when architecture changes
|
|
34
|
-
- Run the verification named in the feature docs
|
|
50
|
+
- Run the verification named in the feature docs
|
|
51
|
+
- Before claiming done: `duaer job .` shows accepted (coach/strict)
|
|
@@ -71,10 +71,17 @@ of remaining work as a new, traceable task** at the bottom of `tasks.md` so that
|
|
|
71
71
|
This is **not** a diff tool and does **not** track changes. It assesses the present state
|
|
72
72
|
of the code relative to the feature's artifacts — no git, no branch comparison, no history.
|
|
73
73
|
|
|
74
|
+
**Delivery stamp:** Always update `FEATURE_DIR/delivery.json` after assessment (see Step 7).
|
|
75
|
+
That file is what `duaer check --gate` reads for machine-checkable acceptance.
|
|
76
|
+
|
|
74
77
|
## Operating Constraints
|
|
75
78
|
|
|
76
|
-
**APPEND-ONLY,
|
|
77
|
-
|
|
79
|
+
**APPEND-ONLY FOR TASKS, STAMP FOR DELIVERY**: The command's writes are limited to:
|
|
80
|
+
|
|
81
|
+
1. appending a new `## Phase N: Convergence` section to `tasks.md` when findings exist;
|
|
82
|
+
2. writing / overwriting `FEATURE_DIR/delivery.json` (the machine-readable accept stamp).
|
|
83
|
+
|
|
84
|
+
It MUST NOT:
|
|
78
85
|
|
|
79
86
|
- modify `spec.md` or `plan.md` in any way;
|
|
80
87
|
- rewrite, renumber, reorder, or delete any existing task (including tasks from a prior
|
|
@@ -83,7 +90,8 @@ of the code relative to the feature's artifacts — no git, no branch comparison
|
|
|
83
90
|
job of `/duaer-implement`.
|
|
84
91
|
|
|
85
92
|
When the codebase already satisfies everything, the command MUST leave `tasks.md`
|
|
86
|
-
**byte-for-byte unchanged** (no empty Convergence header) and
|
|
93
|
+
**byte-for-byte unchanged** (no empty Convergence header) and still write
|
|
94
|
+
`delivery.json` with `status: "accepted"`.
|
|
87
95
|
|
|
88
96
|
**Constitution Authority**: The project constitution (`.duaer/memory/constitution.md`) is
|
|
89
97
|
**non-negotiable**. Code that violates a MUST principle is the highest-severity finding and
|
|
@@ -221,20 +229,54 @@ Append to the **end** of `tasks.md`, per the append contract:
|
|
|
221
229
|
`CRITICAL`.
|
|
222
230
|
4. Never reuse or renumber existing IDs. If a prior Convergence phase exists, add a new,
|
|
223
231
|
separately-numbered one below it — do not touch the old one.
|
|
232
|
+
5. **Write / overwrite** `FEATURE_DIR/delivery.json`:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"schemaVersion": 1,
|
|
237
|
+
"status": "open",
|
|
238
|
+
"outcome": "tasks_appended",
|
|
239
|
+
"checkedAt": "<ISO-8601 UTC>",
|
|
240
|
+
"openTasks": <count of - [ ] in tasks.md after append>,
|
|
241
|
+
"findings": <number of findings appended>,
|
|
242
|
+
"source": "duaer-converge"
|
|
243
|
+
}
|
|
244
|
+
```
|
|
224
245
|
|
|
225
246
|
**If there are no actionable findings** (`converged` outcome):
|
|
226
247
|
|
|
227
248
|
- Do **not** modify `tasks.md` at all — no empty phase header.
|
|
249
|
+
- **Write / overwrite** `FEATURE_DIR/delivery.json` with:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"schemaVersion": 1,
|
|
254
|
+
"status": "accepted",
|
|
255
|
+
"outcome": "converged",
|
|
256
|
+
"checkedAt": "<ISO-8601 UTC>",
|
|
257
|
+
"openTasks": 0,
|
|
258
|
+
"findings": 0,
|
|
259
|
+
"source": "duaer-converge"
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Set `openTasks` to the count of unchecked `- [ ]` lines currently in `tasks.md`
|
|
264
|
+
(must be `0` for `status: "accepted"`; if any remain, treat as `tasks_appended`
|
|
265
|
+
path instead — do not stamp accepted).
|
|
228
266
|
- Report: **"✅ Converged — the implementation satisfies the spec, plan, and tasks."**
|
|
229
267
|
- Include the summary counts of what was checked.
|
|
230
|
-
|
|
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.
|
|
231
270
|
### 8. Provide Next Actions (Handoff)
|
|
232
271
|
|
|
233
272
|
- On `tasks_appended`: state how many tasks were appended under which phase, and recommend
|
|
234
273
|
running `/duaer-implement` to complete them; note that a follow-up converge
|
|
235
|
-
run will find fewer or no remaining items.
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
run will find fewer or no remaining items. Remind: `delivery.json` is `open` —
|
|
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.
|
|
238
280
|
|
|
239
281
|
### 9. Check for extension hooks
|
|
240
282
|
|
|
@@ -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.
|
|
@@ -35,3 +35,14 @@ Do not expand scope without updating the spec and tasks.
|
|
|
35
35
|
|
|
36
36
|
Mark tasks complete only when the stated verification (tests, manual checks,
|
|
37
37
|
or converge) has been performed or explicitly waived in the feature docs.
|
|
38
|
+
|
|
39
|
+
**Job handoff (not a git lock):** before telling the user the work is finished,
|
|
40
|
+
run `duaer job .`. Read `.duaer/delivery-policy.json`:
|
|
41
|
+
|
|
42
|
+
- `coach` (default): if the active job is not `accepted`, warn and guide — do
|
|
43
|
+
**not** claim "done"
|
|
44
|
+
- `strict`: refuse to report delivery complete until accepted
|
|
45
|
+
- `off`: record only
|
|
46
|
+
|
|
47
|
+
Converge still stamps `delivery.json`. Judgment Spec↔code remains
|
|
48
|
+
agent-assisted; the stamp makes the **job** checkable.
|
package/ADOPT.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Adopt duaer-spec
|
|
2
2
|
|
|
3
|
-
Onboard a **digital employee
|
|
4
|
-
|
|
3
|
+
Onboard a **digital employee**: install the Duaer delivery OS (method) and the
|
|
4
|
+
agent-ops contract. Controllability is **job handoff**, not a git merge lock.
|
|
5
5
|
|
|
6
6
|
## Recommended: CLI
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ From the target project (Node 18+):
|
|
|
11
11
|
npx duaer-spec init --here
|
|
12
12
|
|
|
13
13
|
# Pin a release
|
|
14
|
-
npx github:fujiezee/duaer-spec@v0.
|
|
14
|
+
npx github:fujiezee/duaer-spec@v0.3.0 duaer init --here
|
|
15
15
|
|
|
16
16
|
# Lite (method only) / ops only / other integration branch
|
|
17
17
|
npx duaer-spec init --here --method
|
|
@@ -22,7 +22,8 @@ From a clone of this repo:
|
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
node bin/duaer.mjs init /path/to/project --all
|
|
25
|
-
node bin/duaer.mjs
|
|
25
|
+
node bin/duaer.mjs job /path/to/project
|
|
26
|
+
node bin/duaer.mjs policy /path/to/project
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
`--force` overwrites managed files.
|
|
@@ -30,10 +31,11 @@ node bin/duaer.mjs check /path/to/project
|
|
|
30
31
|
Then:
|
|
31
32
|
|
|
32
33
|
1. **Orient** — edit `.duaer/memory/constitution.md` and `project-context.md`
|
|
33
|
-
2. **
|
|
34
|
-
3. **Assign → work → accept** — `/duaer-specify` →
|
|
34
|
+
2. **Policy** — `duaer policy .` (default `coach`; optional `off` | `strict`)
|
|
35
|
+
3. **Assign → work → accept → handoff** — `/duaer-specify` → … → `/duaer-converge` → `duaer job .`
|
|
35
36
|
|
|
36
|
-
A job without Spec is not assigned.
|
|
37
|
+
A job without Spec is not assigned. An unaccepted active job must not be reported
|
|
38
|
+
as done under `coach`/`strict`. Git is not blocked by default.
|
|
37
39
|
|
|
38
40
|
## Manual copy (optional)
|
|
39
41
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-09-15
|
|
4
|
+
|
|
5
|
+
### Job handoff (not a repo lock)
|
|
6
|
+
|
|
7
|
+
- Default policy **`coach`**: guide the employee; do not claim "done" until the
|
|
8
|
+
**active job** is accepted — git merge is not blocked
|
|
9
|
+
- `.duaer/delivery-policy.json` modes: `off` | `coach` | `strict`
|
|
10
|
+
- `.duaer/active-job.json` set by `/duaer-specify`; `duaer job` reports status
|
|
11
|
+
- `duaer policy` to show/set mode
|
|
12
|
+
- `duaer check` defaults to active-job scope; `--strict` / policy `strict` optional
|
|
13
|
+
- `--gate` kept as deprecated alias for `--strict` (job handoff, not CI)
|
|
14
|
+
- `--all-jobs` optional; historical features no longer fail the default path
|
|
15
|
+
- Docs/rules/constitution demote merge-gate language
|
|
16
|
+
|
|
17
|
+
### E2E
|
|
18
|
+
|
|
19
|
+
- E2E-004–007 for coach default, strict optional, active-job scope
|
|
20
|
+
|
|
21
|
+
## 0.2.0 — 2026-09-15
|
|
22
|
+
|
|
23
|
+
### Controllable delivery (machine gate)
|
|
24
|
+
|
|
25
|
+
- `/duaer-converge` writes `.duaer/specs/<feature>/delivery.json`
|
|
26
|
+
(`accepted` | `open`)
|
|
27
|
+
- `duaer check` reports workplace **and** delivery status
|
|
28
|
+
- `duaer check --gate` fails on missing Spec, open tasks, or non-accepted stamp
|
|
29
|
+
- Constitution, DUADER, README, ADOPT, and merge checklist require the gate
|
|
30
|
+
- Honest scope: gate checks **handoff state**; Spec↔code judgment remains
|
|
31
|
+
agent-assisted
|
|
32
|
+
|
|
33
|
+
### E2E
|
|
34
|
+
|
|
35
|
+
- E2E-003 / E2E-004 / E2E-005 for delivery report and gate
|
|
36
|
+
|
|
3
37
|
## 0.1.1 — 2026-09-15
|
|
4
38
|
|
|
5
39
|
### Docs / product identity
|
|
@@ -30,9 +64,6 @@ First public methodology release.
|
|
|
30
64
|
### Install
|
|
31
65
|
|
|
32
66
|
```bash
|
|
33
|
-
# From GitHub (works now)
|
|
34
67
|
npx github:fujiezee/duaer-spec@v0.1.0 duaer init --here
|
|
35
|
-
|
|
36
|
-
# From npm (after publish)
|
|
37
68
|
npx duaer-spec init --here
|
|
38
69
|
```
|
package/DUADER.md
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
# Duaer methodology
|
|
2
2
|
|
|
3
3
|
**duaer-spec** owns this method. Treat AI coding agents as **digital employees**:
|
|
4
|
-
you assign a Brief (Spec), they follow a fixed work order, and
|
|
5
|
-
counts after **converge** accepts
|
|
4
|
+
you assign a Brief (Spec), they follow a fixed work order, and the **job** only
|
|
5
|
+
counts as done after **converge** accepts it — then `duaer job` shows accepted.
|
|
6
6
|
|
|
7
7
|
Artifacts live under `.duaer/`; Cursor skills are `duaer-*` at `.cursor/skills/`.
|
|
8
8
|
|
|
9
9
|
**Precedence:** root [`AGENTS.md`](AGENTS.md) / [`docs/agent/`](docs/agent/) win
|
|
10
10
|
over anything here for isolation, commits, and Issue/PR gates.
|
|
11
11
|
|
|
12
|
+
**Handoff is job-level**, not a repository merge lock. Policy:
|
|
13
|
+
`.duaer/delivery-policy.json` (`off` | `coach` | `strict`, default `coach`).
|
|
14
|
+
|
|
12
15
|
## Read before work
|
|
13
16
|
|
|
14
17
|
1. `.duaer/memory/constitution.md` — process principles
|
|
15
18
|
2. `.duaer/memory/project-context.md` — as-is implementation truth
|
|
16
19
|
3. `.duaer/memory/testing.md` — verification expectations
|
|
17
|
-
4. Active feature under `.duaer/specs/<nnn-slug>/`
|
|
20
|
+
4. Active job: `.duaer/active-job.json` → feature under `.duaer/specs/<nnn-slug>/`
|
|
18
21
|
|
|
19
22
|
## Standard path
|
|
20
23
|
|
|
21
24
|
**Full (recommended):**
|
|
22
25
|
|
|
23
26
|
1. `/duaer-constitution` — only when principles change
|
|
24
|
-
2. `/duaer-specify` — **Assign**:
|
|
27
|
+
2. `/duaer-specify` — **Assign**: Brief + set `active-job.json`
|
|
25
28
|
3. `/duaer-clarify` — optional
|
|
26
29
|
4. `/duaer-plan` — technical plan aligned with project-context
|
|
27
30
|
5. `/duaer-checklist` — optional quality checklist
|
|
28
31
|
6. `/duaer-tasks` — checkbox task breakdown
|
|
29
32
|
7. `/duaer-analyze` — optional consistency check
|
|
30
33
|
8. `/duaer-implement` — implement tasks only
|
|
31
|
-
9. `/duaer-converge` — **Accept**: compare
|
|
34
|
+
9. `/duaer-converge` — **Accept**: compare to Spec; write `delivery.json`
|
|
35
|
+
10. `duaer job .` — **Handoff**: may this job be reported done?
|
|
32
36
|
|
|
33
|
-
**Small feature:** specify → plan → tasks → implement → converge
|
|
37
|
+
**Small feature:** specify → plan → tasks → implement → converge → `duaer job`
|
|
34
38
|
|
|
35
|
-
**Hotfix:** specify (mark hotfix) → tasks → implement → converge
|
|
36
|
-
Never skip specify (assign) or converge (accept).
|
|
39
|
+
**Hotfix:** specify (mark hotfix) → tasks → implement → converge → `duaer job`
|
|
40
|
+
Never skip specify (assign) or converge (accept). Under coach/strict, never claim
|
|
41
|
+
"done" while `duaer job` is unfinished.
|
|
37
42
|
|
|
38
43
|
## Install
|
|
39
44
|
|
|
40
45
|
```bash
|
|
41
46
|
npx duaer-spec init --here
|
|
47
|
+
duaer policy . coach
|
|
42
48
|
```
|
|
43
49
|
|
|
44
|
-
Or see [`ADOPT.md`](ADOPT.md).
|
|
45
|
-
installs this repository's method files into the target project.
|
|
50
|
+
Or see [`ADOPT.md`](ADOPT.md).
|
|
46
51
|
|
|
47
52
|
## Feature directory shape
|
|
48
53
|
|
|
49
54
|
```text
|
|
55
|
+
.duaer/delivery-policy.json
|
|
56
|
+
.duaer/active-job.json
|
|
50
57
|
.duaer/specs/<nnn-slug>/
|
|
51
|
-
spec.md
|
|
52
|
-
plan.md
|
|
53
|
-
tasks.md
|
|
54
|
-
|
|
58
|
+
spec.md # Brief
|
|
59
|
+
plan.md
|
|
60
|
+
tasks.md
|
|
61
|
+
delivery.json # accepted | open
|
|
55
62
|
```
|
|
56
63
|
|
|
57
64
|
## Related
|
package/README.md
CHANGED
|
@@ -3,86 +3,83 @@
|
|
|
3
3
|
**Duaer** is a delivery operating system for AI coding agents treated as
|
|
4
4
|
**digital employees**.
|
|
5
5
|
|
|
6
|
-
Agents write the code. Duaer makes the **handoff controllable**: every job
|
|
7
|
-
Brief (Spec), follows a fixed work order, and only counts as done
|
|
8
|
-
|
|
6
|
+
Agents write the code. Duaer makes the **job handoff controllable**: every job
|
|
7
|
+
has a Brief (Spec), follows a fixed work order, and only counts as done when
|
|
8
|
+
that **job** is accepted — not after a lucky chat.
|
|
9
9
|
|
|
10
|
-
Cursor is the labor. Duaer is hire → assign → accept.
|
|
10
|
+
Cursor is the labor. Duaer is hire → assign → accept.
|
|
11
|
+
**Not** a git merge lock. Controllability is job etiquette + optional strictness.
|
|
11
12
|
|
|
12
13
|
## Install (onboard the employee)
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
npx duaer-spec init --here
|
|
16
|
-
# or pin: npx github:fujiezee/duaer-spec@v0.
|
|
17
|
+
# or pin: npx github:fujiezee/duaer-spec@v0.3.0 duaer init --here
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
|
-
# Lite: method only (personal / small changes)
|
|
21
21
|
npx duaer-spec init --here --method
|
|
22
|
-
|
|
23
|
-
# Full ops on another integration branch
|
|
24
22
|
npx duaer-spec init --here --ops --branch develop
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
```bash
|
|
26
|
+
duaer policy . # default: coach
|
|
27
|
+
duaer job . # active job handoff status
|
|
28
|
+
duaer check .
|
|
29
|
+
```
|
|
30
|
+
|
|
28
31
|
Details: [`ADOPT.md`](ADOPT.md)
|
|
29
32
|
|
|
30
33
|
## How you run them
|
|
31
34
|
|
|
32
35
|
| Step | Meaning | Command |
|
|
33
36
|
|---|---|---|
|
|
34
|
-
| **Hire** | Install SOP
|
|
35
|
-
| **Assign** |
|
|
37
|
+
| **Hire** | Install SOP into the project | `duaer init` |
|
|
38
|
+
| **Assign** | Brief + set active job | `/duaer-specify` |
|
|
36
39
|
| **Work** | Plan, break down, implement | `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` |
|
|
37
|
-
| **Accept** |
|
|
40
|
+
| **Accept** | Converge stamps `delivery.json` | `/duaer-converge` |
|
|
41
|
+
| **Handoff** | See if this job may be reported done | `duaer job .` |
|
|
42
|
+
|
|
43
|
+
### Policy (job-level)
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
Stored in `.duaer/delivery-policy.json`:
|
|
46
|
+
|
|
47
|
+
| Mode | Behavior |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `off` | Record only |
|
|
50
|
+
| `coach` | **Default** — guide; do not claim "done" until accepted |
|
|
51
|
+
| `strict` | Refuse to report done; `duaer check --strict` exits 1 on unfinished job |
|
|
52
|
+
|
|
53
|
+
Scope is the **active job** (`.duaer/active-job.json`), not every historical feature.
|
|
54
|
+
Git commit/merge is never blocked by default.
|
|
41
55
|
|
|
42
56
|
## Two layers
|
|
43
57
|
|
|
44
58
|
| Layer | Role | Where |
|
|
45
59
|
|---|---|---|
|
|
46
|
-
| **Agent ops** | How the employee
|
|
47
|
-
| **Duaer method** | What to build
|
|
60
|
+
| **Agent ops** | How the employee may operate | [`AGENTS.md`](AGENTS.md), [`docs/agent/`](docs/agent/) |
|
|
61
|
+
| **Duaer method** | What to build | [`DUADER.md`](DUADER.md), [`.duaer/`](.duaer/), skills |
|
|
48
62
|
|
|
49
|
-
When they conflict, **agent ops win
|
|
63
|
+
When they conflict, **agent ops win**.
|
|
50
64
|
|
|
51
65
|
## Default loop
|
|
52
66
|
|
|
53
67
|
```text
|
|
54
|
-
constitution → specify → plan → tasks → implement → converge
|
|
68
|
+
constitution → specify → plan → tasks → implement → converge → duaer job
|
|
55
69
|
```
|
|
56
70
|
|
|
57
|
-
Small change: `specify → plan → tasks → implement → converge`
|
|
58
|
-
Hotfix: `specify (hotfix) → tasks → implement → converge`
|
|
59
|
-
|
|
60
71
|
Slash skills: `/duaer-specify`, `/duaer-plan`, `/duaer-tasks`, `/duaer-implement`,
|
|
61
72
|
`/duaer-converge`, …
|
|
62
73
|
|
|
63
74
|
## Layout
|
|
64
75
|
|
|
65
76
|
```text
|
|
66
|
-
bin/duaer.mjs
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
ADOPT.md Onboarding guide
|
|
71
|
-
.duaer/ Memory, templates, workflows, scripts
|
|
72
|
-
.cursor/rules/ Agent-ops + Duaer rules
|
|
73
|
-
.cursor/skills/ duaer-* skills
|
|
74
|
-
docs/agent/ Workflow detail + checklists
|
|
75
|
-
docs/maintaining.md How maintainers evolve the method
|
|
76
|
-
docs/npm-trusted-publishing.md npm Trusted Publishing (OIDC) setup
|
|
77
|
-
examples/ Optional product overlays (not defaults)
|
|
77
|
+
bin/duaer.mjs CLI (init | check | job | policy | version)
|
|
78
|
+
.duaer/delivery-policy.json off | coach | strict
|
|
79
|
+
.duaer/active-job.json current job pointer
|
|
80
|
+
.duaer/specs/<feature>/ Brief + tasks + delivery.json
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
## Defaults (this repo)
|
|
81
|
-
|
|
82
|
-
- Integration branch: **`main`**
|
|
83
|
-
- One request → one branch + one worktree → merge → delete worktree
|
|
84
|
-
- Push only when explicitly requested
|
|
85
|
-
|
|
86
83
|
## Learn more
|
|
87
84
|
|
|
88
85
|
Method: [`DUADER.md`](DUADER.md) · Agent ops: [`AGENTS.md`](AGENTS.md)
|
package/bin/duaer.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* duaer — CLI for the duaer-spec delivery OS (digital employees)
|
|
4
4
|
*
|
|
5
5
|
* duaer init [dir] [--all|--method|--ops] [--force] [--branch <name>]
|
|
6
|
-
* duaer check [dir]
|
|
6
|
+
* duaer check [dir] [--workplace|--delivery|--job|--all-jobs|--strict]
|
|
7
|
+
* duaer job [dir]
|
|
8
|
+
* duaer policy [dir] [off|coach|strict]
|
|
7
9
|
* duaer version
|
|
8
10
|
* duaer help
|
|
9
11
|
*/
|
|
@@ -24,29 +26,48 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
24
26
|
const PKG_ROOT = resolve(__dirname, '..')
|
|
25
27
|
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
26
28
|
|
|
29
|
+
const POLICY_MODES = new Set(['off', 'coach', 'strict'])
|
|
30
|
+
|
|
27
31
|
const USAGE = `duaer — Delivery OS for AI digital employees (duaer-spec ${PKG.version})
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
Job-level handoff (not a repo merge lock). Default policy: coach.
|
|
30
34
|
|
|
31
35
|
Usage:
|
|
32
|
-
duaer init [dir] [options]
|
|
33
|
-
duaer check [dir]
|
|
34
|
-
duaer
|
|
35
|
-
duaer
|
|
36
|
+
duaer init [dir] [options] Hire / onboard into a project
|
|
37
|
+
duaer check [dir] [options] Workplace + active-job handoff
|
|
38
|
+
duaer job [dir] Show the active job status
|
|
39
|
+
duaer policy [dir] [off|coach|strict] Show or set handoff policy
|
|
40
|
+
duaer version Print version
|
|
41
|
+
duaer help Show this help
|
|
36
42
|
|
|
37
43
|
Init options:
|
|
38
44
|
--all Full hire: agent ops + method (default)
|
|
39
|
-
--method Lite: method only
|
|
40
|
-
--ops Ops only
|
|
45
|
+
--method Lite: method only
|
|
46
|
+
--ops Ops only
|
|
41
47
|
--force Overwrite existing managed files
|
|
42
48
|
--branch <n> Integration branch for baseline note (default: main)
|
|
43
49
|
--here Same as dir=.
|
|
44
50
|
|
|
51
|
+
Check options:
|
|
52
|
+
--workplace Only verify install files
|
|
53
|
+
--delivery Alias for active-job handoff report
|
|
54
|
+
--job Active job only (default for delivery)
|
|
55
|
+
--all-jobs Report every feature under .duaer/specs/
|
|
56
|
+
--strict Exit 1 if active job is not accepted (optional)
|
|
57
|
+
--gate Deprecated alias for --strict (job handoff, not CI)
|
|
58
|
+
|
|
59
|
+
Policy modes (stored in .duaer/delivery-policy.json):
|
|
60
|
+
off Record only; never fail handoff
|
|
61
|
+
coach Default — guide / warn; do not claim "done" until accepted
|
|
62
|
+
strict Agents must not report delivery complete until accepted;
|
|
63
|
+
duaer check exits 1 on unfinished active job
|
|
64
|
+
|
|
45
65
|
Examples:
|
|
46
66
|
npx duaer-spec init --here
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
duaer job .
|
|
68
|
+
duaer policy . coach
|
|
49
69
|
duaer check .
|
|
70
|
+
duaer check . --strict
|
|
50
71
|
`
|
|
51
72
|
|
|
52
73
|
function parseArgs(argv) {
|
|
@@ -57,6 +78,12 @@ function parseArgs(argv) {
|
|
|
57
78
|
mode: 'all',
|
|
58
79
|
force: false,
|
|
59
80
|
branch: 'main',
|
|
81
|
+
workplace: false,
|
|
82
|
+
delivery: false,
|
|
83
|
+
job: false,
|
|
84
|
+
allJobs: false,
|
|
85
|
+
strict: false,
|
|
86
|
+
policyMode: null,
|
|
60
87
|
}
|
|
61
88
|
const rest = args.slice(1)
|
|
62
89
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -66,11 +93,20 @@ function parseArgs(argv) {
|
|
|
66
93
|
else if (a === '--ops') out.mode = 'ops'
|
|
67
94
|
else if (a === '--force') out.force = true
|
|
68
95
|
else if (a === '--here') out.dir = '.'
|
|
96
|
+
else if (a === '--workplace') out.workplace = true
|
|
97
|
+
else if (a === '--delivery') out.delivery = true
|
|
98
|
+
else if (a === '--job') out.job = true
|
|
99
|
+
else if (a === '--all-jobs') out.allJobs = true
|
|
100
|
+
else if (a === '--strict' || a === '--gate') out.strict = true
|
|
69
101
|
else if (a === '--branch') {
|
|
70
102
|
out.branch = rest[++i]
|
|
71
103
|
if (!out.branch) throw new Error('--branch requires a value')
|
|
104
|
+
} else if (POLICY_MODES.has(a) && out.cmd === 'policy') {
|
|
105
|
+
out.policyMode = a
|
|
72
106
|
} else if (a.startsWith('-')) {
|
|
73
107
|
throw new Error(`Unknown flag: ${a}`)
|
|
108
|
+
} else if (out.cmd === 'policy' && POLICY_MODES.has(a)) {
|
|
109
|
+
out.policyMode = a
|
|
74
110
|
} else {
|
|
75
111
|
out.dir = a
|
|
76
112
|
}
|
|
@@ -89,7 +125,6 @@ function copyPath(from, to, { force }) {
|
|
|
89
125
|
if (existsSync(to) && !force) {
|
|
90
126
|
const st = statSync(to)
|
|
91
127
|
if (st.isDirectory()) {
|
|
92
|
-
// merge: copy children carefully
|
|
93
128
|
for (const name of readdirSync(from)) {
|
|
94
129
|
copyPath(join(from, name), join(to, name), { force })
|
|
95
130
|
}
|
|
@@ -113,10 +148,62 @@ function writeIfNeeded(path, content, { force }) {
|
|
|
113
148
|
console.log(`wrote ${path}`)
|
|
114
149
|
}
|
|
115
150
|
|
|
151
|
+
function readJson(path, fallback = null) {
|
|
152
|
+
if (!existsSync(path)) return fallback
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
155
|
+
} catch {
|
|
156
|
+
return fallback
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function defaultPolicy() {
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
mode: 'coach',
|
|
164
|
+
scope: 'active',
|
|
165
|
+
notes:
|
|
166
|
+
'Job-level handoff. off=record only; coach=guide (default); strict=do not claim done until accepted. Not a git merge lock.',
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function policyPath(target) {
|
|
171
|
+
return join(target, '.duaer', 'delivery-policy.json')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function activeJobPath(target) {
|
|
175
|
+
return join(target, '.duaer', 'active-job.json')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function loadPolicy(target) {
|
|
179
|
+
const p = readJson(policyPath(target), null)
|
|
180
|
+
if (!p) return defaultPolicy()
|
|
181
|
+
const mode = POLICY_MODES.has(p.mode) ? p.mode : 'coach'
|
|
182
|
+
return { ...defaultPolicy(), ...p, mode }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writePolicy(target, mode) {
|
|
186
|
+
const next = { ...defaultPolicy(), mode, updatedAt: new Date().toISOString() }
|
|
187
|
+
ensureDir(join(target, '.duaer'))
|
|
188
|
+
writeFileSync(policyPath(target), JSON.stringify(next, null, 2) + '\n')
|
|
189
|
+
return next
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writeDefaultPolicy(target, { force }) {
|
|
193
|
+
const path = policyPath(target)
|
|
194
|
+
if (existsSync(path) && !force) {
|
|
195
|
+
console.log(`skip (exists): ${path}`)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
writeFileSync(path, JSON.stringify(defaultPolicy(), null, 2) + '\n')
|
|
199
|
+
console.log(' .duaer/delivery-policy.json (mode=coach)')
|
|
200
|
+
}
|
|
201
|
+
|
|
116
202
|
function installMethod(target, opts) {
|
|
117
203
|
console.log('Installing Duaer method…')
|
|
118
204
|
copyPath(join(PKG_ROOT, '.duaer'), join(target, '.duaer'), opts)
|
|
119
205
|
console.log(' .duaer/')
|
|
206
|
+
writeDefaultPolicy(target, opts)
|
|
120
207
|
|
|
121
208
|
ensureDir(join(target, '.cursor', 'skills'))
|
|
122
209
|
const skillsRoot = join(PKG_ROOT, '.cursor', 'skills')
|
|
@@ -223,21 +310,123 @@ function cmdInit(opts) {
|
|
|
223
310
|
console.log(`
|
|
224
311
|
Hired.
|
|
225
312
|
|
|
226
|
-
Next (digital employee loop):
|
|
313
|
+
Next (digital employee loop — job handoff, not a repo lock):
|
|
227
314
|
1. Orient — edit .duaer/memory/constitution.md and project-context.md
|
|
228
|
-
2.
|
|
229
|
-
3. Assign — /duaer-specify
|
|
315
|
+
2. Policy — duaer policy . (default coach; optional: off | strict)
|
|
316
|
+
3. Assign — /duaer-specify (sets active job + Brief)
|
|
230
317
|
4. Work — /duaer-plan → /duaer-tasks → /duaer-implement
|
|
231
|
-
5. Accept — /duaer-converge (
|
|
232
|
-
6.
|
|
318
|
+
5. Accept — /duaer-converge (stamps delivery.json)
|
|
319
|
+
6. Job — duaer job . (see if this job can be reported done)
|
|
320
|
+
7. Ops — AGENTS.md wins over DUADER.md when they conflict
|
|
233
321
|
|
|
234
|
-
No Spec = not assigned.
|
|
235
|
-
|
|
322
|
+
No Spec = not assigned. Unaccepted active job = do not claim "done".
|
|
323
|
+
Git merge is not blocked by default.
|
|
236
324
|
`)
|
|
237
325
|
}
|
|
238
326
|
|
|
239
|
-
function
|
|
240
|
-
|
|
327
|
+
function countOpenTasks(tasksPath) {
|
|
328
|
+
if (!existsSync(tasksPath)) return null
|
|
329
|
+
const text = readFileSync(tasksPath, 'utf8')
|
|
330
|
+
const open = (text.match(/^\s*-\s*\[\s\]/gm) || []).length
|
|
331
|
+
const done = (text.match(/^\s*-\s*\[x\]/gim) || []).length
|
|
332
|
+
return { open, done }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readDelivery(path) {
|
|
336
|
+
if (!existsSync(path)) return null
|
|
337
|
+
try {
|
|
338
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
339
|
+
} catch {
|
|
340
|
+
return { status: 'invalid', error: 'unreadable delivery.json' }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function featureVerdict(name, dir) {
|
|
345
|
+
const specPath = join(dir, 'spec.md')
|
|
346
|
+
const tasksPath = join(dir, 'tasks.md')
|
|
347
|
+
const deliveryPath = join(dir, 'delivery.json')
|
|
348
|
+
const tasks = countOpenTasks(tasksPath)
|
|
349
|
+
const delivery = readDelivery(deliveryPath)
|
|
350
|
+
const hasSpec = existsSync(specPath)
|
|
351
|
+
const stamp = delivery?.status || 'none'
|
|
352
|
+
let verdict = 'accepted'
|
|
353
|
+
const blockers = []
|
|
354
|
+
|
|
355
|
+
if (!hasSpec) {
|
|
356
|
+
verdict = 'blocked'
|
|
357
|
+
blockers.push('missing spec.md')
|
|
358
|
+
}
|
|
359
|
+
if (tasks && tasks.open > 0) {
|
|
360
|
+
verdict = 'blocked'
|
|
361
|
+
blockers.push(`${tasks.open} open task(s)`)
|
|
362
|
+
}
|
|
363
|
+
if (stamp === 'open' || stamp === 'invalid') {
|
|
364
|
+
verdict = 'blocked'
|
|
365
|
+
blockers.push(`delivery.json status=${stamp}`)
|
|
366
|
+
}
|
|
367
|
+
if (hasSpec && tasks && tasks.open === 0 && (stamp === 'none' || !delivery)) {
|
|
368
|
+
verdict = 'unaccepted'
|
|
369
|
+
blockers.push('no delivery.json (run /duaer-converge)')
|
|
370
|
+
}
|
|
371
|
+
if (hasSpec && !tasks && (stamp === 'none' || !delivery)) {
|
|
372
|
+
verdict = 'unaccepted'
|
|
373
|
+
blockers.push('no tasks.md / no delivery.json')
|
|
374
|
+
}
|
|
375
|
+
if (stamp === 'accepted' && tasks && tasks.open === 0 && hasSpec) {
|
|
376
|
+
verdict = 'accepted'
|
|
377
|
+
blockers.length = 0
|
|
378
|
+
} else if (stamp === 'accepted' && (!tasks || tasks.open === 0) && hasSpec && !tasks) {
|
|
379
|
+
// accepted stamp with no tasks file — treat as accepted only if stamp says so
|
|
380
|
+
// keep unaccepted path above for !tasks && no stamp
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Stamp accepted but open tasks remain → blocked (fake stamp)
|
|
384
|
+
if (stamp === 'accepted' && tasks && tasks.open > 0) {
|
|
385
|
+
verdict = 'blocked'
|
|
386
|
+
blockers.length = 0
|
|
387
|
+
blockers.push('delivery.json accepted but open tasks remain')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { name, hasSpec, tasks, stamp, verdict, blockers }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function listFeatures(target) {
|
|
394
|
+
const root = join(target, '.duaer', 'specs')
|
|
395
|
+
if (!existsSync(root)) return []
|
|
396
|
+
return readdirSync(root)
|
|
397
|
+
.filter((name) => {
|
|
398
|
+
try {
|
|
399
|
+
return statSync(join(root, name)).isDirectory()
|
|
400
|
+
} catch {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
.sort()
|
|
405
|
+
.map((name) => featureVerdict(name, join(root, name)))
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function resolveActiveJobName(target, features) {
|
|
409
|
+
const marker = readJson(activeJobPath(target), null)
|
|
410
|
+
if (marker?.feature && features.some((f) => f.name === marker.feature)) {
|
|
411
|
+
return marker.feature
|
|
412
|
+
}
|
|
413
|
+
if (features.length === 0) return null
|
|
414
|
+
// Prefer newest directory name (lexicographic works for NNN-slug)
|
|
415
|
+
return features[features.length - 1].name
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function printFeature(f, { label } = {}) {
|
|
419
|
+
const taskInfo = f.tasks
|
|
420
|
+
? `tasks open=${f.tasks.open} done=${f.tasks.done}`
|
|
421
|
+
: 'no tasks.md'
|
|
422
|
+
const prefix = label ? `${label} ` : ''
|
|
423
|
+
console.log(
|
|
424
|
+
`${prefix}${f.verdict.padEnd(10)} ${f.name} spec=${f.hasSpec ? 'yes' : 'NO'} stamp=${f.stamp} ${taskInfo}`,
|
|
425
|
+
)
|
|
426
|
+
for (const b of f.blockers) console.log(` · ${b}`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function checkWorkplace(target) {
|
|
241
430
|
const checks = [
|
|
242
431
|
['.duaer/memory/constitution.md', 'method'],
|
|
243
432
|
['DUADER.md', 'method'],
|
|
@@ -248,20 +437,178 @@ function cmdCheck(dir) {
|
|
|
248
437
|
['docs/agent/workflow.md', 'ops'],
|
|
249
438
|
]
|
|
250
439
|
let missing = 0
|
|
251
|
-
console.log(
|
|
440
|
+
console.log('## Workplace\n')
|
|
252
441
|
for (const [rel, kind] of checks) {
|
|
253
442
|
const ok = existsSync(join(target, rel))
|
|
254
443
|
console.log(`${ok ? 'ok ' : 'MISS'} [${kind}] ${rel}`)
|
|
255
444
|
if (!ok) missing++
|
|
256
445
|
}
|
|
257
|
-
|
|
258
|
-
|
|
446
|
+
return missing
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function checkDelivery(target, { allJobs, policy }) {
|
|
450
|
+
const features = listFeatures(target)
|
|
451
|
+
const activeName = resolveActiveJobName(target, features)
|
|
452
|
+
const active = features.find((f) => f.name === activeName) || null
|
|
453
|
+
|
|
454
|
+
console.log('\n## Handoff (jobs)\n')
|
|
455
|
+
console.log(`policy: ${policy.mode} scope: ${allJobs ? 'all' : 'active'}`)
|
|
456
|
+
|
|
457
|
+
if (features.length === 0) {
|
|
458
|
+
console.log('ok no features under .duaer/specs/ (nothing to hand off)')
|
|
459
|
+
return { blocked: 0, unaccepted: 0, features: 0, active: null, unfinished: false }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (allJobs) {
|
|
463
|
+
for (const f of features) {
|
|
464
|
+
printFeature(f, { label: f.name === activeName ? '★' : ' ' })
|
|
465
|
+
}
|
|
466
|
+
} else if (active) {
|
|
467
|
+
printFeature(active, { label: '★' })
|
|
468
|
+
console.log(`\nactive job: ${active.name} (from .duaer/active-job.json or latest spec)`)
|
|
469
|
+
} else {
|
|
470
|
+
console.log('ok no resolvable active job')
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const focus = allJobs ? features : active ? [active] : []
|
|
474
|
+
let blocked = 0
|
|
475
|
+
let unaccepted = 0
|
|
476
|
+
for (const f of focus) {
|
|
477
|
+
if (f.verdict === 'blocked') blocked++
|
|
478
|
+
if (f.verdict === 'unaccepted') unaccepted++
|
|
479
|
+
}
|
|
480
|
+
const unfinished = blocked + unaccepted > 0
|
|
481
|
+
return { blocked, unaccepted, features: focus.length, active, unfinished }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function handoffAdvice(policy, unfinished, strictFlag) {
|
|
485
|
+
if (!unfinished) {
|
|
486
|
+
console.log('\nActive job accepted — employee may report delivery complete.')
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
if (policy.mode === 'off' && !strictFlag) {
|
|
490
|
+
console.log('\nHandoff incomplete (policy=off) — recorded only.')
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
if (policy.mode === 'coach' && !strictFlag) {
|
|
494
|
+
console.log(`
|
|
495
|
+
Coach: this job is not accepted yet.
|
|
496
|
+
→ close open tasks, run /duaer-converge, then duaer job .
|
|
497
|
+
→ do not tell the user the work is "done" until status=accepted
|
|
498
|
+
→ git is not blocked; this is job etiquette, not a merge lock
|
|
499
|
+
`)
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
console.log(`
|
|
503
|
+
Strict: active job not accepted — exit 1.
|
|
504
|
+
→ /duaer-converge until delivery.json status=accepted and tasks clear
|
|
505
|
+
`)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function cmdCheck(opts) {
|
|
509
|
+
const target = resolve(opts.dir)
|
|
510
|
+
const policy = loadPolicy(target)
|
|
511
|
+
const strictFlag = opts.strict || policy.mode === 'strict'
|
|
512
|
+
|
|
513
|
+
// default: workplace + delivery; --workplace alone; job/delivery/strict/all-jobs → delivery
|
|
514
|
+
let wp = false
|
|
515
|
+
let del = false
|
|
516
|
+
if (opts.workplace && !opts.delivery && !opts.job && !opts.allJobs && !opts.strict) {
|
|
517
|
+
wp = true
|
|
518
|
+
} else if (opts.delivery || opts.job || opts.allJobs || opts.strict) {
|
|
519
|
+
del = true
|
|
520
|
+
if (opts.workplace) wp = true
|
|
521
|
+
} else {
|
|
522
|
+
wp = true
|
|
523
|
+
del = true
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log(`Checking ${target}${strictFlag ? ' (strict handoff)' : ''}\n`)
|
|
527
|
+
|
|
528
|
+
let workplaceMissing = 0
|
|
529
|
+
if (wp) workplaceMissing = checkWorkplace(target)
|
|
530
|
+
|
|
531
|
+
let delivery = { unfinished: false }
|
|
532
|
+
if (del) {
|
|
533
|
+
delivery = checkDelivery(target, { allJobs: opts.allJobs, policy })
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let fail = false
|
|
537
|
+
if (workplaceMissing) {
|
|
538
|
+
console.log(`\n${workplaceMissing} workplace file(s) missing — run: duaer init ${opts.dir} --all`)
|
|
539
|
+
fail = true
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (del) {
|
|
543
|
+
handoffAdvice(policy, delivery.unfinished, opts.strict)
|
|
544
|
+
if (delivery.unfinished && strictFlag) {
|
|
545
|
+
fail = true
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!fail) {
|
|
550
|
+
if (wp && !workplaceMissing && !del) {
|
|
551
|
+
console.log('\nWorkplace ready.')
|
|
552
|
+
} else if (!delivery.unfinished) {
|
|
553
|
+
console.log('\nCheck ok.')
|
|
554
|
+
} else {
|
|
555
|
+
console.log('\nCheck ok (handoff still open under coach/off — see above).')
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
259
558
|
process.exitCode = 1
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function cmdJob(opts) {
|
|
563
|
+
const target = resolve(opts.dir)
|
|
564
|
+
const policy = loadPolicy(target)
|
|
565
|
+
const features = listFeatures(target)
|
|
566
|
+
const activeName = resolveActiveJobName(target, features)
|
|
567
|
+
const active = features.find((f) => f.name === activeName) || null
|
|
568
|
+
|
|
569
|
+
console.log(`Active job @ ${target}`)
|
|
570
|
+
console.log(`policy: ${policy.mode}\n`)
|
|
571
|
+
if (!active) {
|
|
572
|
+
console.log('No active job. Assign with /duaer-specify (writes .duaer/active-job.json).')
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
printFeature(active)
|
|
576
|
+
if (active.verdict === 'accepted') {
|
|
577
|
+
console.log('\nHandoff script: "✅ Job accepted — Brief satisfied per converge; ready for your review."')
|
|
260
578
|
} else {
|
|
261
|
-
console.log('\
|
|
579
|
+
console.log('\nHandoff script: "This job is not accepted yet — Spec/tasks/converge still open."')
|
|
580
|
+
if (policy.mode !== 'off') {
|
|
581
|
+
console.log('Do not claim delivery complete until duaer job shows accepted.')
|
|
582
|
+
}
|
|
262
583
|
}
|
|
263
584
|
}
|
|
264
585
|
|
|
586
|
+
function cmdPolicy(opts) {
|
|
587
|
+
const target = resolve(opts.dir)
|
|
588
|
+
if (opts.policyMode) {
|
|
589
|
+
if (!POLICY_MODES.has(opts.policyMode)) {
|
|
590
|
+
throw new Error(`Unknown policy mode: ${opts.policyMode}`)
|
|
591
|
+
}
|
|
592
|
+
const next = writePolicy(target, opts.policyMode)
|
|
593
|
+
console.log(`Wrote ${policyPath(target)}`)
|
|
594
|
+
console.log(`mode: ${next.mode}`)
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
const policy = loadPolicy(target)
|
|
598
|
+
const exists = existsSync(policyPath(target))
|
|
599
|
+
console.log(`Policy @ ${target}`)
|
|
600
|
+
console.log(`file: ${exists ? policyPath(target) : '(defaults; run init or: duaer policy . coach)'}`)
|
|
601
|
+
console.log(`mode: ${policy.mode}`)
|
|
602
|
+
console.log(`scope: ${policy.scope || 'active'}`)
|
|
603
|
+
console.log(`
|
|
604
|
+
Modes:
|
|
605
|
+
off record only
|
|
606
|
+
coach guide; do not claim done until accepted (default)
|
|
607
|
+
strict check fails + agents must not report done until accepted
|
|
608
|
+
Not a git merge lock.
|
|
609
|
+
`)
|
|
610
|
+
}
|
|
611
|
+
|
|
265
612
|
function main() {
|
|
266
613
|
let opts
|
|
267
614
|
try {
|
|
@@ -278,7 +625,13 @@ function main() {
|
|
|
278
625
|
cmdInit(opts)
|
|
279
626
|
break
|
|
280
627
|
case 'check':
|
|
281
|
-
cmdCheck(opts
|
|
628
|
+
cmdCheck(opts)
|
|
629
|
+
break
|
|
630
|
+
case 'job':
|
|
631
|
+
cmdJob(opts)
|
|
632
|
+
break
|
|
633
|
+
case 'policy':
|
|
634
|
+
cmdPolicy(opts)
|
|
282
635
|
break
|
|
283
636
|
case 'version':
|
|
284
637
|
case '--version':
|
|
@@ -94,6 +94,7 @@ See [spec update guidance](workflow.md#3-spec-update-guidance).
|
|
|
94
94
|
|
|
95
95
|
## 6. Merge / PR
|
|
96
96
|
|
|
97
|
+
- [ ] Active job handoff considered: `duaer job .` (coach/strict: do not claim done if unfinished).
|
|
97
98
|
- [ ] Branch refreshed against latest `main`.
|
|
98
99
|
- [ ] Merged into local `main` (and/or remote PR/MR when required).
|
|
99
100
|
- [ ] Only this request's logical changes included.
|
|
@@ -121,6 +122,7 @@ See [spec update guidance](workflow.md#3-spec-update-guidance).
|
|
|
121
122
|
| 3 | Impacted specs updated | [R1](workflow.md#r1--spec-first--spec-sync) |
|
|
122
123
|
| 4 | E2E docs updated or confirmed unnecessary | [R3](workflow.md#r3--e2e-coverage-doc) |
|
|
123
124
|
| 5 | Targeted validation done or waived with reason | Development loop |
|
|
125
|
+
| 5a | Active job handoff via `duaer job` (coach/strict) | Delivery OS — not a git lock |
|
|
124
126
|
| 6 | Conventional commits | [R2](workflow.md#r2--commit-per-change) |
|
|
125
127
|
| 7 | No secrets or local data | [§4](workflow.md#4-what-never-to-commit) |
|
|
126
128
|
| 8 | Merged into `main`; worktree and branch removed | [R4](workflow.md#r4--request-branch--worktree--merge-gate) · [§6.1](#61-merge-cleanup) |
|
|
@@ -27,6 +27,11 @@ the table for their app.
|
|
|
27
27
|
|---|---|---|
|
|
28
28
|
| E2E-001 | After `duaer init`, CLI prints hire → assign → accept next steps | manual |
|
|
29
29
|
| E2E-002 | README positions Duaer as delivery OS for digital employees | manual |
|
|
30
|
+
| E2E-003 | `duaer check --job` reports open tasks on active job | manual |
|
|
31
|
+
| E2E-004 | Default `coach` policy: unfinished job warns but check exits 0 | manual |
|
|
32
|
+
| E2E-005 | After converge stamps `accepted`, `duaer job` 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 |
|
|
30
35
|
|
|
31
36
|
## Traceability
|
|
32
37
|
|
|
@@ -34,5 +39,10 @@ Keep this matrix current when scenarios or specs change.
|
|
|
34
39
|
|
|
35
40
|
| Scenario | Spec / feature | Notes |
|
|
36
41
|
|---|---|---|
|
|
37
|
-
| E2E-001 | `bin/duaer.mjs` init banner |
|
|
42
|
+
| E2E-001 | `bin/duaer.mjs` init banner | Onboarding |
|
|
38
43
|
| E2E-002 | `README.md` / `ADOPT.md` | Product identity |
|
|
44
|
+
| E2E-003 | active job delivery check | Open `- [ ]` |
|
|
45
|
+
| E2E-004 | `delivery-policy.json` coach | Not a repo lock |
|
|
46
|
+
| E2E-005 | `/duaer-converge` → `delivery.json` | Accept stamp |
|
|
47
|
+
| E2E-006 | policy strict | Optional hardness |
|
|
48
|
+
| E2E-007 | `--all-jobs` vs active | Scope |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "duaer-spec",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Delivery OS for AI digital employees: Spec briefs, agent ops
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Delivery OS for AI digital employees: job handoff (coach/strict), Spec briefs, agent ops",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"duaer": "bin/duaer.mjs",
|