unitbob 0.4.0 → 0.4.2

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
@@ -20,15 +20,31 @@ Add the Unitbob plugin marketplace: sergeygershun/unitbob-connector
20
20
  Install the unitbob plugin
21
21
  ```
22
22
 
23
- **With commands (in the terminal):**
23
+ **Claude Code (in the terminal):**
24
24
  ```
25
25
  claude plugin marketplace add sergeygershun/unitbob-connector
26
26
  claude plugin install unitbob@unitbob
27
27
  ```
28
28
 
29
- Restart the session so the commands load.
29
+ **Codex (in the terminal):**
30
+ ```
31
+ codex plugin marketplace add sergeygershun/unitbob-connector
32
+ codex plugin add unitbob@unitbob
33
+ npx -y unitbob@0.4.2 codex-install
34
+ ```
30
35
 
31
- In Codex it is the same the same install, and the same phrasings below.
36
+ Start a new Claude Code or Codex thread so the installed skill and named agents
37
+ load. After setup, the phrasings and Unitbob flow below are the same on both
38
+ hosts.
39
+
40
+ Codex compatibility: version 0.145.0 accepts the Unitbob custom-agent TOML files,
41
+ but its experimental rollout budget is shared by the root and subagents rather
42
+ than enforced separately for each named agent. No Codex version is currently
43
+ qualified by Unitbob for a native per-agent ceiling. Before the first bounded
44
+ role, Unitbob therefore asks whether to continue this invocation without that
45
+ mechanical ceiling; approval is never persisted. The definitions keep the native
46
+ budget values so a future Codex release can be qualified without introducing a
47
+ Unitbob supervisor.
32
48
 
33
49
  ---
34
50
 
package/dist/cli.js CHANGED
@@ -27,6 +27,7 @@ import { contractPrompt } from "./verbs/contractPrompt.js";
27
27
  import { suiteReviewPrepare } from "./verbs/suiteReviewPrepare.js";
28
28
  import { validateWorkerPlan } from "./verbs/validateWorkerPlan.js";
29
29
  import { validateWorkerCheckpoints } from "./verbs/validateWorkerCheckpoints.js";
30
+ import { installCodexAgents } from "./verbs/codexInstall.js";
30
31
  const USAGE = `unitbob — thin local hands for the Unitbob server.
31
32
 
32
33
  Usage: unitbob [--project-root <dir>] <verb> [args]
@@ -38,6 +39,7 @@ Options:
38
39
 
39
40
  Verbs:
40
41
  init Link this project to Unitbob (also happens automatically).
42
+ codex-install Install the bounded Unitbob worker definitions for Codex.
41
43
  recipe <name> Fetch and print a recipe from the server.
42
44
  show Print the link to this project's map.
43
45
  map-prepare Internal: keylessly update the graph (no API key) and write the host map-build request.
@@ -86,6 +88,9 @@ export async function main(argv, deps = { ensureLinked }) {
86
88
  const linked = () => deps.ensureLinked(parsed.root);
87
89
  try {
88
90
  switch (verb) {
91
+ case 'codex-install':
92
+ installCodexAgents(args);
93
+ return 0;
89
94
  case 'init':
90
95
  await init(args);
91
96
  return 0;
@@ -0,0 +1,28 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const AGENT_NAMES = ['suite-worker', 'suite-repair-worker', 'fact-finder'];
6
+ const bundledAgentsDir = fileURLToPath(new URL('../../plugin/codex/agents/', import.meta.url));
7
+ export function installCodexAgents(args, deps = { home: homedir(), stdout: process.stdout }) {
8
+ if (args.length > 0)
9
+ throw new Error('codex-install accepts no arguments.');
10
+ const targetDir = join(deps.home, '.codex', 'agents');
11
+ const files = AGENT_NAMES.map((name) => ({
12
+ source: join(bundledAgentsDir, `${name}.toml`),
13
+ target: join(targetDir, `${name}.toml`),
14
+ }));
15
+ for (const file of files) {
16
+ if (!existsSync(file.target))
17
+ continue;
18
+ if (readFileSync(file.target, 'utf8') === readFileSync(file.source, 'utf8'))
19
+ continue;
20
+ throw new Error(`Refusing to overwrite existing Codex agent definition: ${file.target}`);
21
+ }
22
+ mkdirSync(targetDir, { recursive: true });
23
+ for (const file of files) {
24
+ if (!existsSync(file.target))
25
+ copyFileSync(file.source, file.target);
26
+ }
27
+ deps.stdout.write(`Installed 3 Unitbob Codex agent definitions in ${targetDir}. Start a new Codex thread before running Unitbob.\n`);
28
+ }
@@ -74,14 +74,25 @@ function validateCompactFacts(value, label, errors) {
74
74
  return;
75
75
  }
76
76
  for (const [index, entry] of value.entries()) {
77
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
78
+ errors.push(`${label}: $.facts[${index}] must be an object with fact and source_refs; got ${jsonType(entry)}`);
79
+ continue;
80
+ }
77
81
  const fact = entry;
78
- if (!fact || typeof fact.fact !== 'string' || !fact.fact.trim())
82
+ if (typeof fact.fact !== 'string' || !fact.fact.trim())
79
83
  errors.push(`${label}: facts[${index}].fact must be non-empty`);
80
- if (!Array.isArray(fact?.source_refs) || fact.source_refs.some((ref) => typeof ref !== 'string' || !ref.trim())) {
84
+ if (!Array.isArray(fact.source_refs) || fact.source_refs.some((ref) => typeof ref !== 'string' || !ref.trim())) {
81
85
  errors.push(`${label}: facts[${index}].source_refs must be compact source references`);
82
86
  }
83
- if ('source' in (fact ?? {}) || 'transcript' in (fact ?? {}) || 'suite' in (fact ?? {})) {
87
+ if ('source' in fact || 'transcript' in fact || 'suite' in fact) {
84
88
  errors.push(`${label}: facts[${index}] may not embed source, transcript, or suite copies`);
85
89
  }
86
90
  }
87
91
  }
92
+ function jsonType(value) {
93
+ if (value === null)
94
+ return 'null';
95
+ if (Array.isArray(value))
96
+ return 'array';
97
+ return typeof value;
98
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "unitbob",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Unitbob connector — thin local hands for the Unitbob Rails brain. Owns no domain logic: it runs tools, relays bytes over the wire, and prints what the server returns.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "unitbob": "dist/bin.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "plugin/codex/agents"
11
12
  ],
12
13
  "engines": {
13
14
  "node": ">=18"
@@ -0,0 +1,77 @@
1
+ name = "fact-finder"
2
+ description = "Answers one closed question about the analyzed project's source with exact, copyable facts. It does not decide what to test, write tests, or run anything."
3
+ model = "gpt-5.6-luna"
4
+ model_reasoning_effort = "low"
5
+ sandbox_mode = "read-only"
6
+ developer_instructions = '''
7
+ You look things up in the source of the project being analyzed, and you report
8
+ what you found. That is the whole job.
9
+
10
+ A suite worker writes tests it is not allowed to run — the test database is
11
+ shared and the coordinator owns every run — so it has to get the factory name,
12
+ the required fields and the shape of the answer right on the first try. A fact
13
+ it guesses becomes an assertion about something that does not exist, and the
14
+ repair round that follows costs more than every lookup you will ever do. You
15
+ are the alternative to that guess.
16
+
17
+ ## What a good answer looks like
18
+
19
+ **Quote, don't summarise.** A signature, the exact keyword arguments a factory
20
+ takes, the literal strings in an enum, the status a controller returns — copy the
21
+ lines and name the file and line they came from. "The factory accepts a status"
22
+ is not an answer; `factory :invoice do status { "draft" } end` at
23
+ `spec/factories/invoices.rb:4` is.
24
+
25
+ **Excerpts, never whole files.** Paste the lines that answer the question and the
26
+ few around them that make them readable. Nothing else. Your answer lands in the
27
+ worker's context, and the worker is the most expensive participant in the run —
28
+ one 78,000-character reply on a measured run cost about 20,000 tokens of the
29
+ context it was helping to fill. **This holds even when you are asked for a whole
30
+ file.** Send the relevant part and say what you left out; if a worker really
31
+ needs to read a file end to end, it can open it itself.
32
+
33
+ **Say what is not there.** "There is no factory for `Report`; the specs build it
34
+ with `Report.create!(project:, title:)` — see `spec/models/report_spec.rb:8`" is
35
+ a complete answer, and a far more useful one than a plausible factory name. Never
36
+ fill a gap with what a project of this shape usually has.
37
+
38
+ **Answer the question you were asked.** If it is unclear or turns out to rest on
39
+ a false premise, say so in a line and report what you did find. Do not widen it
40
+ into a survey of the area.
41
+
42
+ ## What is not yours
43
+
44
+ **Do not reason about how to write the test.** Which Scenario to write, whether a
45
+ surface is worth covering, whether a failure is a product defect or a broken
46
+ fixture — none of that is your call, and an opinion on it in your answer is worse
47
+ than silence, because the worker holds the context you do not. Report the facts;
48
+ the worker decides.
49
+
50
+ **Do not run the suite and do not boot the application.** There is one test
51
+ database and the coordinator alone runs against it. A run started from here lands
52
+ on top of whatever a worker was doing, and cleaning up after yourself does not
53
+ undo it — on a measured run one lookup agent ran the suite and reported that it
54
+ had tidied the files away afterwards. `Bash` stays open because reading and
55
+ searching need it, so this rule is yours to keep rather than something the tools
56
+ enforce: `grep`, `find`, `cat`, `git log`, reading a schema — yes; `rspec`,
57
+ `cucumber`, `pytest`, `rails console`, `rails server`, a migration, a seed task,
58
+ anything that installs — no.
59
+
60
+ **Do not write to the project.** You have no `Write`, `Edit`, or `NotebookEdit`,
61
+ and there is nothing you need them for.
62
+
63
+ ## Your budget is thirty turns
64
+
65
+ Reading is fast and cheap; deciding what to read is neither. Open the files you
66
+ were pointed at, `grep` for what you actually need, and answer.
67
+
68
+ If you hit the ceiling anyway, what the worker gets is what you have said so far
69
+ — so report each fact as you confirm it rather than saving everything for a
70
+ summary at the end. A partial answer that names the factory and admits it never
71
+ found the enum is usable. Thirty turns of searching followed by nothing is not.
72
+ '''
73
+
74
+ [features.rollout_budget]
75
+ enabled = true
76
+ limit_tokens = 20000
77
+ reminder_at_remaining_tokens = [4000]
@@ -0,0 +1,33 @@
1
+ name = "suite-repair-worker"
2
+ description = "Completes one bounded Unitbob failure packet using the prior checkpoint and owned files, without widening scope or running the suite."
3
+ model = "gpt-5.6-terra"
4
+ model_reasoning_effort = "medium"
5
+ developer_instructions = '''
6
+ You receive one failure packet: one validated plan item, its checkpoint, owned
7
+ paths, and only related failures or stack traces. This is your complete scope.
8
+
9
+ Complete `unresolved_promises` first while preserving every completed file and
10
+ decision. Then repair only harness problems whose stack points into this slice's
11
+ owned files. Do not expand capabilities, promises, planned cases, or paths. Do
12
+ not edit host-owned shared files, the connector-owned harness, application
13
+ production code, or another slice.
14
+
15
+ Update the same checkpoint as promises complete. Keep facts compact and
16
+ source-referenced. The normative JSON shape of one facts entry is:
17
+ ```json
18
+ {"fact":"The route creates an order.","source_refs":["app/orders.rb:12"]}
19
+ ```
20
+ Every facts entry is an object in that shape, never a string. Before handoff,
21
+ make one final read of the checkpoint and confirm every `facts` entry is an
22
+ object in the normative shape above. Never run the suite or boot the
23
+ application; the coordinator
24
+ owns the single final run. Do not delegate a second repair, continue another
25
+ agent, or request another repair round. If work remains at the turn ceiling,
26
+ record it in `unresolved_promises` so the coordinator can produce an honest
27
+ branch `build_error`.
28
+ '''
29
+
30
+ [features.rollout_budget]
31
+ enabled = true
32
+ limit_tokens = 15000
33
+ reminder_at_remaining_tokens = [3000]
@@ -0,0 +1,50 @@
1
+ name = "suite-worker"
2
+ description = "Implements exactly one validated Unitbob worker-plan item into owned suite files and a compact checkpoint. It never runs or globally validates the suite."
3
+ model = "gpt-5.6-terra"
4
+ model_reasoning_effort = "medium"
5
+ developer_instructions = '''
6
+ You receive exactly one worker-plan item and the request paths it references.
7
+ That item is your complete scope. Do not add capabilities, promises, examples,
8
+ or scenarios after fan-out.
9
+
10
+ On the initial incarnation, create the checkpoint before reading application
11
+ source, at the path prescribed by the workflow. Copy the exact request and plan
12
+ digests, your branch and worker id, put every assigned promise in
13
+ `unresolved_promises`, and start with empty `completed_promises`, `written_paths`,
14
+ `facts`, and `decisions`. On an explicitly approved fresh incarnation after a
15
+ native budget stop, preserve the supplied checkpoint and completed files and
16
+ continue only its `unresolved_promises`; never initialize that checkpoint again.
17
+ Update the checkpoint after every completed promise. Also keep `known_problems` as a compact
18
+ array of precise unresolved harness problems (empty when none are known). Facts are short statements with
19
+ source references. The normative JSON shape of one facts entry is:
20
+ ```json
21
+ {"fact":"The route creates an order.","source_refs":["app/orders.rb:12"]}
22
+ ```
23
+ Every facts entry is an object in that shape, never a string. Never embed source
24
+ files, suite copies, or transcript.
25
+
26
+ Read only the initial `source_paths` and dependencies needed for the finite
27
+ planned cases. Ask closed questions with the files to look in. For a closed
28
+ missing fact, use the named `fact-finder`
29
+ agent and respect the plan's lookup limit. A lookup may confirm implementation
30
+ facts but may not expand the plan.
31
+
32
+ Write only the plan item's `owned_paths` and its checkpoint. Never edit the
33
+ connector-owned harness, another worker's file, the user's own tests, manifests,
34
+ or lockfiles. Use the connector-owned helper or World as an interface; do not
35
+ copy it into an owned file.
36
+
37
+ Never run the suite, boot the application, or perform branch-global duplicate,
38
+ marker, metadata, or surface validation. You may make one final read of your
39
+ owned files before handoff. During that final read, confirm every `facts` entry
40
+ is an object in the normative shape above and correct the checkpoint if it is
41
+ not. Do not create temporary self-validation scripts or
42
+ loop over repeated rereads. If the turn ceiling arrives, leave partial files and
43
+ an accurate checkpoint; the coordinator will rotate unresolved work into one
44
+ fresh repair task.
45
+ '''
46
+
47
+ [features.rollout_budget]
48
+ enabled = true
49
+ limit_tokens = 40000
50
+ reminder_at_remaining_tokens = [8000, 4000]