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.
@@ -1,16 +1,32 @@
1
1
  ---
2
- description: Features and hotfixes must go through Duaer; no code from verbal asks alone
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: assign a Brief (Spec) before code; accept
9
- only via converge. See `DUADER.md` and `.duaer/memory/constitution.md`.
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 before merge
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, NEVER REWRITE**: The command's **only** write is appending a new
77
- `## Phase N: Convergence` section to `tasks.md`. It MUST NOT:
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 report a clean result.
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
- - On `converged`: recommend proceeding to review / opening a PR. No further implement pass
237
- is needed for this feature's specified scope.
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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "mode": "coach",
4
+ "scope": "active",
5
+ "notes": "Job-level handoff. off=record only; coach=guide (default); strict=do not claim done until accepted. Not a git merge lock."
6
+ }
@@ -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** into another repository: install the Duaer
4
- delivery OS (method) and the agent-ops contract (how they are allowed to work).
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.1.1 duaer init --here
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 check /path/to/project
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. **Confirm workplace** — `docs/baseline.md` (integration branch)
34
- 3. **Assign → work → accept** — `/duaer-specify` → `/duaer-plan` → `/duaer-tasks` → `/duaer-implement` `/duaer-converge`
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. A change that fails converge is not accepted.
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 delivery only
5
- counts after **converge** accepts the result against that Brief.
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>/` when one exists
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**: what / why / acceptance (not stack trivia)
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 result to Spec; append remaining tasks if gaps
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). No third-party Spec CLI is required — `duaer`
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 # Brief
52
- plan.md # after /duaer-plan
53
- tasks.md # after /duaer-tasks
54
- # research / contracts as needed
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 has a
7
- Brief (Spec), follows a fixed work order, and only counts as done after
8
- acceptance — not after a lucky chat.
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.1.1 duaer init --here
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
- Verify: `npx duaer-spec check .`
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 + gates into the repo | `duaer init` |
35
- | **Assign** | Write the Brief (what / why / acceptance) | `/duaer-specify` |
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** | Compare result to Spec; gaps become more work | `/duaer-converge` + `duaer check` |
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
- Never skip **Assign** or **Accept**. Hotfix may shorten the middle; Spec and
40
- converge stay.
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 is allowed to operate | [`AGENTS.md`](AGENTS.md), [`docs/agent/`](docs/agent/) |
47
- | **Duaer method** | What to build (Spec → Plan → Tasks → Implement → Converge) | [`DUADER.md`](DUADER.md), [`.duaer/`](.duaer/), [`.cursor/skills/`](.cursor/skills/) |
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** — process beats improvisation.
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 CLI (duaer init | check | version)
67
- package.json npm package metadata
68
- AGENTS.md Agent-ops contract (how employees operate)
69
- DUADER.md Methodology conventions (how work is briefed)
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
- Hire agents into a repo, assign Briefs (Specs), accept only when converge passes.
33
+ Job-level handoff (not a repo merge lock). Default policy: coach.
30
34
 
31
35
  Usage:
32
- duaer init [dir] [options] Hire / onboard into a project
33
- duaer check [dir] Verify install (workplace ready)
34
- duaer version Print version
35
- duaer help Show this help
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 (.duaer, skills, DUADER.md, duaer-spec rule)
40
- --ops Ops only (AGENTS.md, docs/agent, ops rules)
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
- npx github:fujiezee/duaer-spec duaer init --here
48
- node bin/duaer.mjs init ../my-app --all
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. Workplace confirm docs/baseline.md (integration branch: ${opts.branch})
229
- 3. Assign — /duaer-specify (Brief: what / why / acceptance)
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 (gaps are not done; append tasks)
232
- 6. Gates AGENTS.md wins over DUADER.md when they conflict
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. Failed converge = not accepted.
235
- See DUADER.md and AGENTS.md in the target project.
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 cmdCheck(dir) {
240
- const target = resolve(dir)
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(`Checking ${target}\n`)
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
- if (missing) {
258
- console.log(`\n${missing} missing — run: duaer init ${dir} --all`)
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('\nWorkplace ready assign with /duaer-specify, accept with /duaer-converge.')
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.dir)
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 | Controllable-delivery onboarding |
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.1.1",
4
- "description": "Delivery OS for AI digital employees: Spec briefs, agent ops, controllable handoffs",
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",