duaer-spec 0.2.0 → 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 -6
- package/.cursor/skills/duaer-converge/SKILL.md +7 -4
- package/.cursor/skills/duaer-specify/SKILL.md +13 -1
- package/.duaer/delivery-policy.json +6 -0
- package/.duaer/memory/constitution.md +10 -4
- package/ADOPT.md +9 -8
- package/CHANGELOG.md +18 -3
- package/DUADER.md +20 -15
- package/README.md +33 -50
- package/bin/duaer.mjs +299 -113
- package/docs/agent/change-checklist.md +2 -2
- package/docs/agent/e2e-test-plan.md +10 -6
- 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,5 +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
|
|
35
|
-
- Before
|
|
50
|
+
- Run the verification named in the feature docs
|
|
51
|
+
- Before claiming done: `duaer job .` shows accepted (coach/strict)
|
|
@@ -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
|
|
|
@@ -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.
|
|
@@ -36,7 +36,13 @@ Do not expand scope without updating the spec and tasks.
|
|
|
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
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,11 +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 →
|
|
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
|
-
|
|
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.
|
|
38
39
|
|
|
39
40
|
## Manual copy (optional)
|
|
40
41
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
3
21
|
## 0.2.0 — 2026-09-15
|
|
4
22
|
|
|
5
23
|
### Controllable delivery (machine gate)
|
|
@@ -46,9 +64,6 @@ First public methodology release.
|
|
|
46
64
|
### Install
|
|
47
65
|
|
|
48
66
|
```bash
|
|
49
|
-
# From GitHub (works now)
|
|
50
67
|
npx github:fujiezee/duaer-spec@v0.1.0 duaer init --here
|
|
51
|
-
|
|
52
|
-
# From npm (after publish)
|
|
53
68
|
npx duaer-spec init --here
|
|
54
69
|
```
|
package/DUADER.md
CHANGED
|
@@ -1,59 +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
|
|
32
|
-
10. `duaer
|
|
34
|
+
9. `/duaer-converge` — **Accept**: compare to Spec; write `delivery.json`
|
|
35
|
+
10. `duaer job .` — **Handoff**: may this job be reported done?
|
|
33
36
|
|
|
34
|
-
**Small feature:** specify → plan → tasks → implement → converge → `
|
|
37
|
+
**Small feature:** specify → plan → tasks → implement → converge → `duaer job`
|
|
35
38
|
|
|
36
|
-
**Hotfix:** specify (mark hotfix) → tasks → implement → converge → `
|
|
37
|
-
Never skip specify (assign)
|
|
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.
|
|
38
42
|
|
|
39
43
|
## Install
|
|
40
44
|
|
|
41
45
|
```bash
|
|
42
46
|
npx duaer-spec init --here
|
|
47
|
+
duaer policy . coach
|
|
43
48
|
```
|
|
44
49
|
|
|
45
|
-
Or see [`ADOPT.md`](ADOPT.md).
|
|
46
|
-
installs this repository's method files into the target project.
|
|
50
|
+
Or see [`ADOPT.md`](ADOPT.md).
|
|
47
51
|
|
|
48
52
|
## Feature directory shape
|
|
49
53
|
|
|
50
54
|
```text
|
|
55
|
+
.duaer/delivery-policy.json
|
|
56
|
+
.duaer/active-job.json
|
|
51
57
|
.duaer/specs/<nnn-slug>/
|
|
52
58
|
spec.md # Brief
|
|
53
|
-
plan.md
|
|
54
|
-
tasks.md
|
|
55
|
-
delivery.json #
|
|
56
|
-
… # research / contracts as needed
|
|
59
|
+
plan.md
|
|
60
|
+
tasks.md
|
|
61
|
+
delivery.json # accepted | open
|
|
57
62
|
```
|
|
58
63
|
|
|
59
64
|
## Related
|
package/README.md
CHANGED
|
@@ -3,100 +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 when
|
|
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
|
-
|
|
28
|
-
|
|
25
|
+
```bash
|
|
26
|
+
duaer policy . # default: coach
|
|
27
|
+
duaer job . # active job handoff status
|
|
28
|
+
duaer check .
|
|
29
|
+
```
|
|
30
|
+
|
|
29
31
|
Details: [`ADOPT.md`](ADOPT.md)
|
|
30
32
|
|
|
31
33
|
## How you run them
|
|
32
34
|
|
|
33
35
|
| Step | Meaning | Command |
|
|
34
36
|
|---|---|---|
|
|
35
|
-
| **Hire** | Install SOP
|
|
36
|
-
| **Assign** |
|
|
37
|
+
| **Hire** | Install SOP into the project | `duaer init` |
|
|
38
|
+
| **Assign** | Brief + set active job | `/duaer-specify` |
|
|
37
39
|
| **Work** | Plan, break down, implement | `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` |
|
|
38
|
-
| **Accept** | Converge stamps `delivery.json
|
|
39
|
-
| **
|
|
40
|
+
| **Accept** | Converge stamps `delivery.json` | `/duaer-converge` |
|
|
41
|
+
| **Handoff** | See if this job may be reported done | `duaer job .` |
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
converge, and the gate stay.
|
|
43
|
+
### Policy (job-level)
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
Stored in `.duaer/delivery-policy.json`:
|
|
45
46
|
|
|
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 |
|
|
47
52
|
|
|
48
|
-
-
|
|
49
|
-
|
|
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.
|
|
53
|
+
Scope is the **active job** (`.duaer/active-job.json`), not every historical feature.
|
|
54
|
+
Git commit/merge is never blocked by default.
|
|
54
55
|
|
|
55
56
|
## Two layers
|
|
56
57
|
|
|
57
58
|
| Layer | Role | Where |
|
|
58
59
|
|---|---|---|
|
|
59
|
-
| **Agent ops** | How the employee
|
|
60
|
-
| **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 |
|
|
61
62
|
|
|
62
|
-
When they conflict, **agent ops win
|
|
63
|
+
When they conflict, **agent ops win**.
|
|
63
64
|
|
|
64
65
|
## Default loop
|
|
65
66
|
|
|
66
67
|
```text
|
|
67
|
-
constitution → specify → plan → tasks → implement → converge →
|
|
68
|
+
constitution → specify → plan → tasks → implement → converge → duaer job
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
Small change: `specify → plan → tasks → implement → converge`
|
|
71
|
-
Hotfix: `specify (hotfix) → tasks → implement → converge`
|
|
72
|
-
|
|
73
71
|
Slash skills: `/duaer-specify`, `/duaer-plan`, `/duaer-tasks`, `/duaer-implement`,
|
|
74
72
|
`/duaer-converge`, …
|
|
75
73
|
|
|
76
74
|
## Layout
|
|
77
75
|
|
|
78
76
|
```text
|
|
79
|
-
bin/duaer.mjs
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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)
|
|
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
|
|
92
81
|
```
|
|
93
82
|
|
|
94
|
-
## Defaults (this repo)
|
|
95
|
-
|
|
96
|
-
- Integration branch: **`main`**
|
|
97
|
-
- One request → one branch + one worktree → merge → delete worktree
|
|
98
|
-
- Push only when explicitly requested
|
|
99
|
-
|
|
100
83
|
## Learn more
|
|
101
84
|
|
|
102
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] [--workplace|--delivery|--
|
|
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,35 +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] [options]
|
|
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
|
|
|
45
51
|
Check options:
|
|
46
52
|
--workplace Only verify install files
|
|
47
|
-
--delivery
|
|
48
|
-
--
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
64
|
|
|
52
65
|
Examples:
|
|
53
66
|
npx duaer-spec init --here
|
|
67
|
+
duaer job .
|
|
68
|
+
duaer policy . coach
|
|
54
69
|
duaer check .
|
|
55
|
-
duaer check . --
|
|
70
|
+
duaer check . --strict
|
|
56
71
|
`
|
|
57
72
|
|
|
58
73
|
function parseArgs(argv) {
|
|
@@ -65,7 +80,10 @@ function parseArgs(argv) {
|
|
|
65
80
|
branch: 'main',
|
|
66
81
|
workplace: false,
|
|
67
82
|
delivery: false,
|
|
68
|
-
|
|
83
|
+
job: false,
|
|
84
|
+
allJobs: false,
|
|
85
|
+
strict: false,
|
|
86
|
+
policyMode: null,
|
|
69
87
|
}
|
|
70
88
|
const rest = args.slice(1)
|
|
71
89
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -77,12 +95,18 @@ function parseArgs(argv) {
|
|
|
77
95
|
else if (a === '--here') out.dir = '.'
|
|
78
96
|
else if (a === '--workplace') out.workplace = true
|
|
79
97
|
else if (a === '--delivery') out.delivery = true
|
|
80
|
-
else if (a === '--
|
|
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
|
|
81
101
|
else if (a === '--branch') {
|
|
82
102
|
out.branch = rest[++i]
|
|
83
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
|
|
84
106
|
} else if (a.startsWith('-')) {
|
|
85
107
|
throw new Error(`Unknown flag: ${a}`)
|
|
108
|
+
} else if (out.cmd === 'policy' && POLICY_MODES.has(a)) {
|
|
109
|
+
out.policyMode = a
|
|
86
110
|
} else {
|
|
87
111
|
out.dir = a
|
|
88
112
|
}
|
|
@@ -124,10 +148,62 @@ function writeIfNeeded(path, content, { force }) {
|
|
|
124
148
|
console.log(`wrote ${path}`)
|
|
125
149
|
}
|
|
126
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
|
+
|
|
127
202
|
function installMethod(target, opts) {
|
|
128
203
|
console.log('Installing Duaer method…')
|
|
129
204
|
copyPath(join(PKG_ROOT, '.duaer'), join(target, '.duaer'), opts)
|
|
130
205
|
console.log(' .duaer/')
|
|
206
|
+
writeDefaultPolicy(target, opts)
|
|
131
207
|
|
|
132
208
|
ensureDir(join(target, '.cursor', 'skills'))
|
|
133
209
|
const skillsRoot = join(PKG_ROOT, '.cursor', 'skills')
|
|
@@ -234,17 +310,17 @@ function cmdInit(opts) {
|
|
|
234
310
|
console.log(`
|
|
235
311
|
Hired.
|
|
236
312
|
|
|
237
|
-
Next (digital employee loop):
|
|
313
|
+
Next (digital employee loop — job handoff, not a repo lock):
|
|
238
314
|
1. Orient — edit .duaer/memory/constitution.md and project-context.md
|
|
239
|
-
2.
|
|
240
|
-
3. Assign — /duaer-specify
|
|
315
|
+
2. Policy — duaer policy . (default coach; optional: off | strict)
|
|
316
|
+
3. Assign — /duaer-specify (sets active job + Brief)
|
|
241
317
|
4. Work — /duaer-plan → /duaer-tasks → /duaer-implement
|
|
242
|
-
5. Accept — /duaer-converge (
|
|
243
|
-
6.
|
|
244
|
-
7.
|
|
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
|
|
245
321
|
|
|
246
|
-
No Spec = not assigned.
|
|
247
|
-
|
|
322
|
+
No Spec = not assigned. Unaccepted active job = do not claim "done".
|
|
323
|
+
Git merge is not blocked by default.
|
|
248
324
|
`)
|
|
249
325
|
}
|
|
250
326
|
|
|
@@ -265,6 +341,55 @@ function readDelivery(path) {
|
|
|
265
341
|
}
|
|
266
342
|
}
|
|
267
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
|
+
|
|
268
393
|
function listFeatures(target) {
|
|
269
394
|
const root = join(target, '.duaer', 'specs')
|
|
270
395
|
if (!existsSync(root)) return []
|
|
@@ -277,53 +402,28 @@ function listFeatures(target) {
|
|
|
277
402
|
}
|
|
278
403
|
})
|
|
279
404
|
.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
|
-
}
|
|
405
|
+
.map((name) => featureVerdict(name, join(root, name)))
|
|
406
|
+
}
|
|
317
407
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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}`)
|
|
327
427
|
}
|
|
328
428
|
|
|
329
429
|
function checkWorkplace(target) {
|
|
@@ -346,51 +446,91 @@ function checkWorkplace(target) {
|
|
|
346
446
|
return missing
|
|
347
447
|
}
|
|
348
448
|
|
|
349
|
-
function checkDelivery(target, {
|
|
449
|
+
function checkDelivery(target, { allJobs, policy }) {
|
|
350
450
|
const features = listFeatures(target)
|
|
351
|
-
|
|
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
|
+
|
|
352
457
|
if (features.length === 0) {
|
|
353
|
-
console.log('ok no features under .duaer/specs/ (nothing to
|
|
354
|
-
return { blocked: 0, unaccepted: 0, features: 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 }
|
|
355
460
|
}
|
|
356
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] : []
|
|
357
474
|
let blocked = 0
|
|
358
475
|
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
|
-
}
|
|
476
|
+
for (const f of focus) {
|
|
368
477
|
if (f.verdict === 'blocked') blocked++
|
|
369
478
|
if (f.verdict === 'unaccepted') unaccepted++
|
|
370
479
|
}
|
|
480
|
+
const unfinished = blocked + unaccepted > 0
|
|
481
|
+
return { blocked, unaccepted, features: focus.length, active, unfinished }
|
|
482
|
+
}
|
|
371
483
|
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
374
501
|
}
|
|
375
|
-
|
|
502
|
+
console.log(`
|
|
503
|
+
Strict: active job not accepted — exit 1.
|
|
504
|
+
→ /duaer-converge until delivery.json status=accepted and tasks clear
|
|
505
|
+
`)
|
|
376
506
|
}
|
|
377
507
|
|
|
378
508
|
function cmdCheck(opts) {
|
|
379
509
|
const target = resolve(opts.dir)
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
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
|
+
}
|
|
383
525
|
|
|
384
|
-
console.log(`Checking ${target}${
|
|
526
|
+
console.log(`Checking ${target}${strictFlag ? ' (strict handoff)' : ''}\n`)
|
|
385
527
|
|
|
386
528
|
let workplaceMissing = 0
|
|
387
|
-
if (
|
|
388
|
-
workplaceMissing = checkWorkplace(target)
|
|
389
|
-
}
|
|
529
|
+
if (wp) workplaceMissing = checkWorkplace(target)
|
|
390
530
|
|
|
391
|
-
let delivery = {
|
|
392
|
-
if (
|
|
393
|
-
delivery = checkDelivery(target, {
|
|
531
|
+
let delivery = { unfinished: false }
|
|
532
|
+
if (del) {
|
|
533
|
+
delivery = checkDelivery(target, { allJobs: opts.allJobs, policy })
|
|
394
534
|
}
|
|
395
535
|
|
|
396
536
|
let fail = false
|
|
@@ -399,36 +539,76 @@ function cmdCheck(opts) {
|
|
|
399
539
|
fail = true
|
|
400
540
|
}
|
|
401
541
|
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
542
|
+
if (del) {
|
|
543
|
+
handoffAdvice(policy, delivery.unfinished, opts.strict)
|
|
544
|
+
if (delivery.unfinished && strictFlag) {
|
|
405
545
|
fail = true
|
|
406
546
|
}
|
|
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
547
|
}
|
|
418
548
|
|
|
419
549
|
if (!fail) {
|
|
420
|
-
if (
|
|
421
|
-
console.log('\
|
|
422
|
-
} else if (
|
|
423
|
-
console.log('\
|
|
550
|
+
if (wp && !workplaceMissing && !del) {
|
|
551
|
+
console.log('\nWorkplace ready.')
|
|
552
|
+
} else if (!delivery.unfinished) {
|
|
553
|
+
console.log('\nCheck ok.')
|
|
424
554
|
} else {
|
|
425
|
-
console.log('\
|
|
555
|
+
console.log('\nCheck ok (handoff still open under coach/off — see above).')
|
|
426
556
|
}
|
|
427
557
|
} else {
|
|
428
558
|
process.exitCode = 1
|
|
429
559
|
}
|
|
430
560
|
}
|
|
431
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."')
|
|
578
|
+
} else {
|
|
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
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
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
|
+
|
|
432
612
|
function main() {
|
|
433
613
|
let opts
|
|
434
614
|
try {
|
|
@@ -447,6 +627,12 @@ function main() {
|
|
|
447
627
|
case 'check':
|
|
448
628
|
cmdCheck(opts)
|
|
449
629
|
break
|
|
630
|
+
case 'job':
|
|
631
|
+
cmdJob(opts)
|
|
632
|
+
break
|
|
633
|
+
case 'policy':
|
|
634
|
+
cmdPolicy(opts)
|
|
635
|
+
break
|
|
450
636
|
case 'version':
|
|
451
637
|
case '--version':
|
|
452
638
|
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) |
|
|
@@ -27,9 +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 --
|
|
31
|
-
| E2E-004 | `
|
|
32
|
-
| E2E-005 | After converge stamps `accepted`,
|
|
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 |
|
|
33
35
|
|
|
34
36
|
## Traceability
|
|
35
37
|
|
|
@@ -37,8 +39,10 @@ Keep this matrix current when scenarios or specs change.
|
|
|
37
39
|
|
|
38
40
|
| Scenario | Spec / feature | Notes |
|
|
39
41
|
|---|---|---|
|
|
40
|
-
| E2E-001 | `bin/duaer.mjs` init banner |
|
|
42
|
+
| E2E-001 | `bin/duaer.mjs` init banner | Onboarding |
|
|
41
43
|
| E2E-002 | `README.md` / `ADOPT.md` | Product identity |
|
|
42
|
-
| E2E-003 |
|
|
43
|
-
| E2E-004 | `
|
|
44
|
+
| E2E-003 | active job delivery check | Open `- [ ]` |
|
|
45
|
+
| E2E-004 | `delivery-policy.json` coach | Not a repo lock |
|
|
44
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",
|