done-done-axi 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -94,13 +94,13 @@ npm install -g --prefix ~/.local --allow-git=all github:umeranjum17/done-done-ax
94
94
 
95
95
  To skip the install, run `npx -y --allow-git=all github:umeranjum17/done-done-axi` wherever this README says `done-done`.
96
96
 
97
- Once 0.1.0 is published, the registry line takes over as the install:
97
+ Once done-done-axi is published to npm, the registry line takes over as the install:
98
98
 
99
99
  ```sh
100
100
  npm install -g --prefix ~/.local done-done-axi
101
101
  ```
102
102
 
103
- <!-- TODO(dd-npm-name): when done-done-axi 0.1.0 is published, flip the primary back to the registry line above and make `npx -y done-done-axi` the skip-the-install path. -->
103
+ <!-- TODO(dd-npm-name): when done-done-axi is published to npm, flip the primary back to the registry line above and make `npx -y done-done-axi` the skip-the-install path. -->
104
104
 
105
105
  **2. Set it up in your app's repo**
106
106
 
@@ -109,7 +109,7 @@ cd your-app
109
109
  done-done init
110
110
  ```
111
111
 
112
- `init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app. It never overwrites an existing config. Evidence lives outside the repo, in `~/.done-done/evidence/<app>`, so it outlives the checkout that captured it. Then describe how to capture proof — swap the command for whatever drives your real app and saves screenshots, recordings or logs:
112
+ `init` writes a minimal `.done-done.yaml` (only if you don't already have one) that names your app. A plain `init` never overwrites an existing config. Evidence lives outside the repo, in `~/.done-done/evidence/<app>`, so it outlives the checkout that captured it. Then describe how to capture proof — swap the command for whatever drives your real app and saves screenshots, recordings or logs:
113
113
 
114
114
  ```yaml
115
115
  journeys:
@@ -119,15 +119,18 @@ journeys:
119
119
 
120
120
  The command runs from the repo root with `DONE_DONE_EVIDENCE_ROOT` (a new folder for this run in the evidence root) and `DONE_DONE_RUN_ID` set, and `${RUN_ID}` in it is replaced. A run that writes nothing into its folder fails.
121
121
 
122
- Optional: `done-done init --agent claude` adds a Claude Code SessionStart hook to the repo's `.claude/settings.json` that shows each session the bare `done-done` state. See the [reference](docs/reference.md#set-up-a-repo). Codex and opencode are not supported yet.
122
+ Optional: `done-done init --agent claude` adds a Claude Code SessionStart hook to the repo's `.claude/settings.json` that shows each session the bare `done-done` state, `done-done init --agent codex` adds the same SessionStart hook to the repo's `.codex/hooks.json` for Codex, and `done-done init --agent opencode` adds a plugin to the repo's `.opencode/plugins/` that adds the state to the first message of each opencode session. See the [reference](docs/reference.md#set-up-a-repo).
123
123
 
124
- Commit the config (and `.claude/settings.json` if you added the hook), because proof only counts from a clean tree:
124
+ Record the recipe in the repo so anyone can re-run the proof, then commit it (and `.claude/settings.json`, `.codex/hooks.json`, or `.opencode/plugins/done-done.js` if you added a hook):
125
125
 
126
126
  ```sh
127
+ done-done init --journey smoke -- 'npm test > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
127
128
  git add .done-done.yaml
128
129
  git commit -m "Add done-done"
129
130
  ```
130
131
 
132
+ A `run -- <command>` whose command matches no recorded journey still runs, but its result hints that the recipe is not in the repo.
133
+
131
134
  **3. Run it, then check it**
132
135
 
133
136
  ```sh
@@ -173,7 +176,7 @@ cp "$(npm root -g)/done-done-axi/integrations/firstmate/merge-gate" "$FM_HOME/co
173
176
  ```
174
177
 
175
178
  - **`brief-include.md`**: Firstmate adds `config/brief-include.md` to every worker brief. It tells the worker to set up done-done, run a journey on its committed change, write a `## Checks` block, pass `check`, and record a `receipt`. The evidence and receipt stay in `~/.done-done/evidence/<app>` after teardown deletes the worktree.
176
- - **`merge-gate`** (optional): `merge-gate <task-id> <worktree> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
179
+ - **`merge-gate`** (optional): `merge-gate <task-id> <worktree|-> <head> <pr-url|->`. In the worktree, it refuses a PR whose head is not `<head>`, then runs `done-done check --from <PR URL or $FM_HOME/data/<task-id>/checks.md> --trusted-ref <default branch>` (`origin/HEAD`, else `origin/main` or `origin/master`, else the local `main` or `master` in a repo with no remote), then `done-done receipt --verify <head>`. Both paths read done-done's rules from your default branch, never from the task's own branch, so merge `.done-done.yaml` there before you rely on it. The gate does not fetch: it reads the local `origin` refs, so run `git fetch origin` before you run it. It needs `done-done` on PATH (the `npm install` line above). A nonzero exit refuses the merge, and done-done's output says why. A worktree of `-` means the task recorded none: the gate does not cd and runs git and done-done in the current directory (the caller's project clone). Upstream Firstmate does not call a merge gate yet, so this file has no effect until it does; you can run it by hand before a merge.
177
180
 
178
181
  Another fleet works the same way: add the include text to whatever your workers read first, and run `merge-gate` before each merge, with `FM_HOME` set to the folder that holds `data/<task-id>/checks.md` for local-only tasks.
179
182
 
@@ -184,7 +187,7 @@ Another fleet works the same way: add the include text to whatever your workers
184
187
 
185
188
  ## What's next
186
189
 
187
- So far: `init`, `run`, `check`, `receipt`, the opt-in screenshot judge and the opt-in Claude Code session hook. Coming next: help writing the proof steps for your app.
190
+ So far: `init`, `run`, `check`, `receipt`, the opt-in screenshot judge and the opt-in Claude Code, Codex and opencode session hooks. Coming next: help writing the proof steps for your app.
188
191
 
189
192
  ## Develop
190
193
 
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { homedir } from 'node:os';
6
6
  import { dirname, isAbsolute, join, matchesGlob, relative, resolve, sep } from 'node:path';
7
7
  import { parseArgs } from 'node:util';
8
8
  import { encode } from '@toon-format/toon';
9
+ import { Document, parseDocument } from 'yaml';
9
10
  import { loadConfig, UsageError } from './config.js';
10
11
  import { judge } from './judge.js';
11
12
  import { checkMedia } from './media.js';
@@ -24,6 +25,7 @@ const CHECKS_TEXT = `Checks block (--from): the newest "## Checks" heading, then
24
25
  // One table drives `--help` and every `<command> --help`: each command's usage lines and the flags it takes.
25
26
  const FLAGS = {
26
27
  '-- <command...>': 'run this command instead of a journey; ${RUN_ID} in it becomes the run id',
28
+ '--journey <name>': 'record this command as a journey in .done-done.yaml; ${RUN_ID} in it becomes the run id',
27
29
  '--open': 'open a run for a hand- or agent-driven proof; prints its id and folder',
28
30
  '--close <run-id>': 'seal an open run and record the files in its folder',
29
31
  '--outcome "<claim>"': 'the outcome the evidence files prove; the files follow as arguments',
@@ -32,7 +34,7 @@ const FLAGS = {
32
34
  '--claim-id <id>': 'the claim the evidence proves; evidence already used for another claim id is reuse (default: the current branch, or the PR branch with --from <PR URL>; required on a detached HEAD)',
33
35
  '--judge': 'a model says whether each PNG/JPEG capture shows the outcome (off by default; or judge.enabled: true)',
34
36
  '--verify <head>': 'read-only: verify the receipt recorded for <head>',
35
- '--agent claude': 'also add a SessionStart hook to .claude/settings.json that shows `done-done` to each session (opt-in)',
37
+ '--agent <agent>': 'also add a SessionStart hook to .claude/settings.json (claude) or .codex/hooks.json (codex) that shows `done-done` to each session, or a plugin to .opencode/plugins that adds it to the first message of each opencode session (opt-in)',
36
38
  };
37
39
  // `more` is the reference text a command needs beyond its flags, shared with the top-level help.
38
40
  const COMMANDS = {
@@ -40,8 +42,11 @@ const COMMANDS = {
40
42
  usage: [
41
43
  ['done-done init', 'write a minimal .done-done.yaml (evidence lives outside the repo)'],
42
44
  ['done-done init --agent claude', 'also show this repo\'s done-done state at each Claude Code session start'],
45
+ ['done-done init --agent codex', 'also show this repo\'s done-done state at each Codex session start'],
46
+ ['done-done init --agent opencode', 'also add this repo\'s done-done state to the first message of each opencode session'],
47
+ ['done-done init --journey <name> -- <command...>', "record a journey's capture recipe in .done-done.yaml"],
43
48
  ],
44
- flags: ['--agent claude'],
49
+ flags: ['--agent <agent>', '--journey <name>', '-- <command...>'],
45
50
  more: [JOURNEYS_TEXT],
46
51
  },
47
52
  run: {
@@ -214,6 +219,14 @@ function closeRun(repo, cfg, rec) {
214
219
  result.help = [reason
215
220
  ? `Fix the capture, commit, then re-run: ${captureHint(cfg)}`
216
221
  : `Check this proof: done-done check --outcome "<what you proved>" ${rel(rec.files[0].path)}`];
222
+ // A command that matches no repo-recorded journey cannot be re-run from the repo recipe.
223
+ // A journey run expands ${RUN_ID} to the run id, so compare with the placeholder restored.
224
+ if (rec.command) {
225
+ const template = rec.command.replaceAll(rec.run_id, '${RUN_ID}');
226
+ const recorded = Object.values(cfg.journeys).some((j) => j.command === template);
227
+ if (!recorded)
228
+ result.help.push(`this command's recipe is not in the repo (.done-done.yaml journeys); record it with: done-done init --journey <name> -- <command>`);
229
+ }
217
230
  emit(result);
218
231
  return reason ? FAIL : PASS;
219
232
  }
@@ -689,9 +702,13 @@ function ensureIgnored(repo, rel) {
689
702
  appendFileSync(file, `${text && !text.endsWith('\n') ? '\n' : ''}${entry}\n`);
690
703
  return true;
691
704
  }
692
- // The Claude Code SessionStart hook `init --agent claude` adds; Claude Code shows its stdout to the session.
705
+ // The session-start hook `init --agent <agent>` adds; the agent shows its stdout to the session.
693
706
  // Without done-done on PATH it prints nothing and exits 0, so it never breaks a session.
694
- const CLAUDE_HOOK = 'command -v done-done >/dev/null && done-done || true';
707
+ // Claude Code reads it from the repo's .claude/settings.json, Codex from the repo's .codex/hooks.json
708
+ // (project scope; Codex docs: <repo>/.codex/hooks.json, SessionStart, matcher omitted matches every start).
709
+ const HOOK_COMMAND = 'command -v done-done >/dev/null && done-done || true';
710
+ const CLAUDE_HOOK = HOOK_COMMAND;
711
+ const CODEX_HOOK = HOOK_COMMAND;
695
712
  // The repo's .claude/settings.json with the hook merged in, keeping every other key and hook; no text when it is already there.
696
713
  function claudeSettings(repo) {
697
714
  const file = join(repo, '.claude', 'settings.json');
@@ -718,15 +735,135 @@ function claudeSettings(repo) {
718
735
  settings.hooks = { ...settings.hooks, SessionStart: [...start, { hooks: [{ type: 'command', command: CLAUDE_HOOK }] }] };
719
736
  return { file, text: `${JSON.stringify(settings, null, 2)}\n` };
720
737
  }
738
+ // The repo's .codex/hooks.json with the hook merged in, keeping every other key and hook; no text when it is already there.
739
+ function codexSettings(repo) {
740
+ const file = join(repo, '.codex', 'hooks.json');
741
+ const bad = (why) => new UsageError(`.codex/hooks.json ${why}`, 'Fix .codex/hooks.json, then re-run: done-done init --agent codex');
742
+ const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
743
+ let settings = {};
744
+ if (existsSync(file)) {
745
+ try {
746
+ settings = JSON.parse(readFileSync(file, 'utf8'));
747
+ }
748
+ catch (e) {
749
+ throw bad(`is not valid JSON (${e.message})`);
750
+ }
751
+ }
752
+ if (!isObject(settings))
753
+ throw bad('is not a JSON object');
754
+ if (settings.hooks !== undefined && !isObject(settings.hooks))
755
+ throw bad('has "hooks" that is not an object');
756
+ const start = settings.hooks?.SessionStart ?? [];
757
+ if (!Array.isArray(start))
758
+ throw bad('has "hooks.SessionStart" that is not a list');
759
+ if (start.some((m) => Array.isArray(m?.hooks) && m.hooks.some((h) => h?.command === CODEX_HOOK)))
760
+ return { file };
761
+ settings.hooks = { ...settings.hooks, SessionStart: [...start, { hooks: [{ type: 'command', command: CODEX_HOOK }] }] };
762
+ return { file, text: `${JSON.stringify(settings, null, 2)}\n` };
763
+ }
764
+ // The opencode session plugin `init --agent opencode` adds: a project-scope file in
765
+ // .opencode/plugins/ (auto-loaded; the home config is never touched) whose chat.message
766
+ // hook runs the session command below on the first message of each session and appends
767
+ // the bare `done-done` state to the message parts. The appended part carries the prt_ id
768
+ // plus the session id from the hook input and the message id from the hook output (opencode
769
+ // passes no messageID to the hook), because opencode validates every
770
+ // part before save and drops the whole first message when one is invalid. Without
771
+ // done-done on PATH it appends nothing and never throws, so it never breaks a session.
772
+ function opencodePlugin(repo) {
773
+ const file = join(repo, '.opencode', 'plugins', 'done-done.js');
774
+ const text = `// Written by \`done-done init --agent opencode\`: shows this repo's bare \`done-done\`
775
+ // state to each opencode session. On the first message of a session the chat.message
776
+ // hook runs \`${CLAUDE_HOOK}\` and appends its output to the message parts.
777
+ // Without done-done on PATH it appends nothing and never throws, so it never breaks
778
+ // a session. Other files in .opencode/plugins/ are untouched; delete this file to opt out.
779
+ import { execFile } from "node:child_process";
780
+ import { randomUUID } from "node:crypto";
781
+
782
+ const COMMAND = "${CLAUDE_HOOK}";
783
+
784
+ const seen = new Set();
785
+ const firstMessage = (sessionID) => {
786
+ if (seen.has(sessionID)) return false;
787
+ seen.add(sessionID);
788
+ return true;
789
+ };
790
+
791
+ const run = (cwd) => new Promise((resolve) => {
792
+ execFile("sh", ["-c", COMMAND], { cwd, encoding: "utf8", timeout: 30000 }, (error, stdout) => resolve(error ? "" : stdout.trim()));
793
+ });
794
+
795
+ export const DoneDonePlugin = async ({ directory }) => {
796
+ return {
797
+ "chat.message": async (input, output) => {
798
+ if (!firstMessage(input.sessionID)) return;
799
+ const state = await run(directory);
800
+ if (state) output.parts.push({ id: \`prt_\${randomUUID()}\`, messageID: output.message.id, sessionID: input.sessionID, type: "text", text: state });
801
+ },
802
+ };
803
+ };
804
+ `;
805
+ if (existsSync(file)) {
806
+ if (readFileSync(file, 'utf8') === text)
807
+ return { file };
808
+ throw new UsageError('.opencode/plugins/done-done.js already exists with other content, so init will not overwrite it', 'Delete .opencode/plugins/done-done.js, then re-run: done-done init --agent opencode');
809
+ }
810
+ return { file, text };
811
+ }
721
812
  function cmdInit(argv) {
722
- const { values } = parseArgs({ args: argv, options: { agent: { type: 'string' } }, allowPositionals: false });
723
- if (values.agent !== undefined && values.agent !== 'claude')
724
- throw new UsageError(`--agent ${values.agent} is not supported; the supported value is claude`, 'Run: done-done init --agent claude');
813
+ const dash = argv.indexOf('--');
814
+ const { values } = parseArgs({
815
+ args: dash < 0 ? argv : argv.slice(0, dash),
816
+ options: { agent: { type: 'string' }, journey: { type: 'string' } },
817
+ allowPositionals: false,
818
+ });
819
+ const parts = dash < 0 ? [] : argv.slice(dash + 1);
820
+ const journey = values.journey ?? null;
821
+ if (values.agent !== undefined && values.agent !== 'claude' && values.agent !== 'codex' && values.agent !== 'opencode')
822
+ throw new UsageError(`--agent ${values.agent} is not supported; the supported values are claude, codex, opencode`, 'Run: done-done init --agent <claude|codex|opencode>');
823
+ if (journey === null && parts.length)
824
+ throw new UsageError('init needs --journey <name> to record a command after --');
825
+ if (journey !== null && !parts.some((p) => p.trim()))
826
+ throw new UsageError(`init --journey ${journey} needs a command after --`);
827
+ if (journey !== null && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(journey))
828
+ throw new UsageError(`invalid journey name ${journey}; use letters, digits, - or _`);
725
829
  const repo = repoRoot();
726
830
  const file = join(repo, '.done-done.yaml');
727
831
  const existed = existsSync(file);
728
832
  const cfg = loadConfig(repo); // validates the existing project config; malformed -> exit 2, no writes
729
- const hook = values.agent ? claudeSettings(repo) : undefined; // likewise validated before any write
833
+ const hook = values.agent === 'claude' ? claudeSettings(repo) : values.agent === 'codex' ? codexSettings(repo) : values.agent === 'opencode' ? opencodePlugin(repo) : undefined; // likewise validated before any write
834
+ if (journey !== null) {
835
+ // Record the capture recipe in the repo so any checkout can re-run the proof.
836
+ // The command is stored verbatim; ${RUN_ID} in it becomes the run id, like a run -- command.
837
+ const command = parts.join(' ');
838
+ const doc = existed ? parseDocument(readFileSync(file, 'utf8')) : new Document({ app: cfg.app });
839
+ const top = (doc.toJS() ?? {});
840
+ if (typeof top !== 'object' || Array.isArray(top))
841
+ throw new UsageError(`${file}: top level must be a mapping`);
842
+ const journeys = top.journeys ?? {};
843
+ if (typeof journeys !== 'object' || Array.isArray(journeys))
844
+ throw new UsageError(`${file}: journeys must be a mapping`);
845
+ if (top.journeys == null)
846
+ doc.set('journeys', doc.createNode({}));
847
+ doc.setIn(['journeys', journey, 'command'], command);
848
+ writeFileSync(`${file}.tmp`, doc.toString({ lineWidth: 0 }));
849
+ renameSync(`${file}.tmp`, file);
850
+ const fresh = loadConfig(repo);
851
+ const rel = under(repo, fresh.evidence.root);
852
+ const added = !!rel && ensureIgnored(repo, rel);
853
+ const root = shown(repo, fresh.evidence.root);
854
+ const check = 'done-done check --outcome "<what you proved>" <files it recorded>';
855
+ emit({
856
+ command: 'init',
857
+ result: 'pass',
858
+ config: existed ? 'updated' : 'written',
859
+ journey,
860
+ app: fresh.app,
861
+ evidence_root: root,
862
+ gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
863
+ help: [`Capture proof: done-done run ${journey}, then: ${check}`],
864
+ });
865
+ return PASS;
866
+ }
730
867
  if (!existed) {
731
868
  // No evidence.root: proof goes to <local_root>/<app>, outside the repo, so it outlives this checkout.
732
869
  // The pinned app name is what lets every checkout of the repo find the same evidence and receipts.
@@ -751,12 +888,23 @@ function cmdInit(argv) {
751
888
  app: cfg.app,
752
889
  evidence_root: root,
753
890
  gitignore: added ? `added ${rel.replace(/\/+$/, '')}/` : 'unchanged',
754
- ...(hook && { claude_hook: hook.text ? 'added to .claude/settings.json' : 'unchanged' }),
891
+ ...(hook &&
892
+ (values.agent === 'codex'
893
+ ? { codex_hook: hook.text ? 'added to .codex/hooks.json' : 'unchanged' }
894
+ : values.agent === 'opencode'
895
+ ? { opencode_plugin: hook.text ? 'added to .opencode/plugins/done-done.js' : 'unchanged' }
896
+ : { claude_hook: hook.text ? 'added to .claude/settings.json' : 'unchanged' })),
755
897
  help: [
756
898
  ...(Object.keys(cfg.journeys).length
757
899
  ? [`Capture proof: ${captureHint(cfg)}, then: ${check}`]
758
900
  : [`Add a journey that drives the real app and saves proof under $DONE_DONE_EVIDENCE_ROOT (each run's own folder in ${root}), e.g. ${journeyExample}`, `Then commit, run \`done-done run smoke\`, and: ${check}`]),
759
- ...(hook?.text ? ['Commit .claude/settings.json: each Claude Code session in this repo then starts with `done-done` output (it needs done-done on PATH)'] : []),
901
+ ...(hook?.text
902
+ ? values.agent === 'codex'
903
+ ? ['Commit .codex/hooks.json: each Codex session in this repo then starts with `done-done` output (it needs done-done on PATH)']
904
+ : values.agent === 'opencode'
905
+ ? ['Commit .opencode/plugins/done-done.js: the first message of each opencode session in this repo then gets `done-done` output added (it needs done-done on PATH)']
906
+ : ['Commit .claude/settings.json: each Claude Code session in this repo then starts with `done-done` output (it needs done-done on PATH)']
907
+ : []),
760
908
  ],
761
909
  });
762
910
  return PASS;
package/docs/reference.md CHANGED
@@ -14,13 +14,33 @@ For the install steps, see the [README](../README.md#use-it-from-your-agent). do
14
14
  done-done init
15
15
  ```
16
16
 
17
- `init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. It never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, and the next step.
17
+ `init` writes a minimal `.done-done.yaml` only when one does not exist: just `app`, the repo directory name, so every checkout of the repo finds the same evidence (see [Where proof lives](#where-proof-lives)). It sets no `evidence.root`, so evidence goes outside the repo. When an existing config puts `evidence.root` inside the repo (an older `root: .proof`), `init` adds it to `.gitignore` when git does not already ignore it. A plain `init` never rewrites the config: re-running is a no-op and a customized `.done-done.yaml` is left byte-for-byte; `.gitignore` gains a line only when the ignore entry is missing. A malformed `.done-done.yaml` makes `init` exit 2 with the file named and untouched. Its result says whether the config was `written` or left `unchanged`, the evidence root, what it added to `.gitignore`, and the next step.
18
+
19
+ Record a journey's capture recipe in the repo so any checkout can re-run the proof:
20
+
21
+ ```sh
22
+ done-done init --journey smoke -- 'echo added: milk > $DONE_DONE_EVIDENCE_ROOT/${RUN_ID}.log'
23
+ ```
24
+
25
+ `init --journey <name> -- <command>` writes `journeys.<name>.command` into `.done-done.yaml` (creating the file with the pinned `app` when missing) and reports `written` or `updated` with the journey name. `${RUN_ID}` in the command becomes the run id, exactly as in `run`. Commit the config: proof only counts from a clean tree.
18
26
 
19
27
  ```sh
20
28
  done-done init --agent claude
21
29
  ```
22
30
 
23
- `--agent claude` also adds a Claude Code SessionStart hook to the repo's `.claude/settings.json` (project scope; your `~/.claude` is never touched), so each session starts with the bare `done-done` state as context. The hook runs `command -v done-done >/dev/null && done-done || true`: without done-done on PATH it prints nothing and exits 0, so it never breaks a session. An existing settings file keeps every other key and hook; re-running adds no duplicate. The result's `claude_hook` says `added to .claude/settings.json` or `unchanged`. Any other `--agent` value exits 2 naming the supported value, and a malformed `.claude/settings.json` exits 2 untouched. Commit `.claude/settings.json` to share the hook.
31
+ `--agent claude` also adds a Claude Code SessionStart hook to the repo's `.claude/settings.json` (project scope; your `~/.claude` is never touched), so each session starts with the bare `done-done` state as context. The hook runs `command -v done-done >/dev/null && done-done || true`: without done-done on PATH it prints nothing and exits 0, so it never breaks a session. An existing settings file keeps every other key and hook; re-running adds no duplicate. The result's `claude_hook` says `added to .claude/settings.json` or `unchanged`. Any other `--agent` value exits 2 naming the supported values, and a malformed `.claude/settings.json` exits 2 untouched. Commit `.claude/settings.json` to share the hook.
32
+
33
+ ```sh
34
+ done-done init --agent codex
35
+ ```
36
+
37
+ `--agent codex` also adds a Codex SessionStart hook to the repo's `.codex/hooks.json` (project scope; your `~/.codex` is never touched), so each session starts with the bare `done-done` state as context. Same contract as the Claude hook: it runs `command -v done-done >/dev/null && done-done || true`, keeps every other key and hook, adds no duplicate on re-run, and the result's `codex_hook` says `added to .codex/hooks.json` or `unchanged`. A malformed `.codex/hooks.json` exits 2 untouched. Commit `.codex/hooks.json` to share the hook.
38
+
39
+ ```sh
40
+ done-done init --agent opencode
41
+ ```
42
+
43
+ `--agent opencode` instead writes a session-start plugin to the repo's `.opencode/plugins/done-done.js` (project scope; your `~/.config/opencode` is never touched), so the bare `done-done` state is added to the first message of each session, not at session open (opencode's plugin hooks cannot add context when a session is created). The plugin remembers sessions in memory, so a resumed session gets the state again after opencode restarts. That `chat.message` hook runs `command -v done-done >/dev/null && done-done || true` and waits for it (up to 30 seconds) before appending the output to the message: without done-done on PATH it appends nothing and never throws, so it never breaks a session. Other files in `.opencode/plugins/` are untouched; re-running adds no duplicate. The result's `opencode_plugin` says `added to .opencode/plugins/done-done.js` or `unchanged`. Delete the file to opt out. Commit it to share the plugin.
24
44
 
25
45
  ### Run a journey
26
46
 
@@ -48,7 +68,7 @@ done-done run review
48
68
  - hashes every file in the run's folder. Every checkout of a repo shares the evidence root, so a run records only its own folder and never another run's files;
49
69
  - writes a run record to `<root>/.done-done/runs/<run-id>.json`.
50
70
 
51
- A non-zero command exit, or a run that wrote nothing, fails.
71
+ A non-zero command exit, or a run that wrote nothing, fails. A run that wrote nothing names its folder and the two names the command gets it under: `DONE_DONE_EVIDENCE_ROOT` and `DONE_DONE_RUN_ID` (or `${RUN_ID}` in the command).
52
72
 
53
73
  For a proof you drive by hand or through an agent:
54
74
 
@@ -59,6 +79,7 @@ done-done run --close <run-id>
59
79
  ```
60
80
 
61
81
  You can also run a one-off command without a journey: `done-done run -- <command>`.
82
+ A run whose command matches no repo-recorded journey says so in its `help`: the recipe is not in the repo, so nobody can re-run the proof from the config. Record it with `done-done init --journey <name> -- <command>` and commit.
62
83
  Run `done-done` outside wrappers that expand `${...}` themselves (`systemd-run` does): put such a wrapper inside the command, or it eats `${RUN_ID}`.
63
84
 
64
85
  ### Check a claim
@@ -5,9 +5,10 @@ Before you report done, load the done-done skill and follow it. If the skill is
5
5
  Run the CLI as `done-done` if it is on PATH; otherwise run `npx -y --allow-git=all github:umeranjum17/done-done-axi`
6
6
  wherever this text or the skill says `done-done`. If done-done cannot run, report that instead of writing proof by hand.
7
7
 
8
- 1. If the repo has no `.done-done.yaml`, run `done-done init`, then add a journey to it that drives the real
9
- app and writes its screenshots, recordings or logs into `$DONE_DONE_EVIDENCE_ROOT` (done-done sets it to the
10
- run's own folder). Commit `.done-done.yaml` and the journey script.
8
+ 1. If the repo has no `.done-done.yaml`, run `done-done init`. Then record a journey that drives the real app and
9
+ writes its screenshots, recordings or logs into `$DONE_DONE_EVIDENCE_ROOT` (done-done sets it to the run's own
10
+ folder): `done-done init --journey <name> -- <command>`, so the recipe lives in the repo and anyone can re-run the
11
+ proof. Commit `.done-done.yaml` and the journey script.
11
12
  2. Commit your change, then run `done-done run <journey>` from the repo root. Proof only counts from a clean,
12
13
  committed tree.
13
14
  3. Write a Checks block (the shape `done-done --help` prints) that cites the run's files by the paths `run` printed:
@@ -1,12 +1,16 @@
1
1
  #!/bin/sh
2
- # merge-gate <task-id> <worktree> <head> <pr-url|->
2
+ # merge-gate <task-id> <worktree|-> <head> <pr-url|->
3
3
  # Run before a merge: it passes only when done-done passes the task's Checks block and the receipt for <head>.
4
4
  # Both a PR and a local-only task (-, read from $FM_HOME/data/<task-id>/checks.md) are checked against the rules
5
5
  # on the default branch, never the task's own branch. A nonzero exit refuses the merge; done-done's output says why.
6
+ # A worktree of - means the task recorded no worktree: do not cd, run git and done-done in the current directory
7
+ # (the caller's project clone).
6
8
  set -eu
7
- [ $# -eq 4 ] || { echo "usage: merge-gate <task-id> <worktree> <head> <pr-url|->" >&2; exit 2; }
9
+ [ $# -eq 4 ] || { echo "usage: merge-gate <task-id> <worktree|-> <head> <pr-url|->" >&2; exit 2; }
8
10
  id=$1 head=$3 pr=$4
9
- cd "$2" || exit 2
11
+ if [ "$2" != - ]; then
12
+ cd "$2" 2>/dev/null || { echo "merge-gate: cannot enter worktree $2" >&2; exit 2; }
13
+ fi
10
14
  ref=$(git symbolic-ref -q --short refs/remotes/origin/HEAD) ||
11
15
  for ref in origin/main origin/master main master; do
12
16
  git rev-parse -q --verify "$ref^{commit}" >/dev/null && break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "done-done-axi",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Proof that the real product works before a task is called done: commit-bound evidence runs and evidence checks.",
6
6
  "type": "module",