@kylecheng3146/agent-ops 0.1.4 → 0.1.6

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.
Files changed (84) hide show
  1. package/README.md +164 -13
  2. package/dist/packages/cli/src/args.js +40 -7
  3. package/dist/packages/cli/src/bin.js +35 -11
  4. package/dist/packages/cli/src/cli.js +7 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +70 -0
  6. package/dist/packages/cli/src/commands/doctor.js +31 -9
  7. package/dist/packages/cli/src/commands/hook.js +33 -16
  8. package/dist/packages/cli/src/commands/init.js +9 -5
  9. package/dist/packages/cli/src/commands/review.js +5 -1
  10. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  11. package/dist/packages/cli/src/commands/update.js +16 -5
  12. package/dist/packages/cli/src/context.js +69 -3
  13. package/dist/packages/cli/src/hook-process.js +231 -16
  14. package/dist/packages/cli/src/loop-entry.js +8 -0
  15. package/dist/packages/cli/src/plan-output.js +9 -4
  16. package/dist/packages/cli/src/public-plan.js +62 -0
  17. package/dist/packages/cli/src/version.js +1 -1
  18. package/dist/packages/cli/src/wizard.js +36 -15
  19. package/dist/runtime/src/adapters/claude/config.js +57 -19
  20. package/dist/runtime/src/adapters/claude/events.js +33 -0
  21. package/dist/runtime/src/adapters/claude/output.js +8 -7
  22. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  23. package/dist/runtime/src/adapters/codex/config.js +66 -13
  24. package/dist/runtime/src/adapters/codex/events.js +33 -0
  25. package/dist/runtime/src/adapters/codex/output.js +10 -0
  26. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  27. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  28. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  29. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  30. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  31. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  32. package/dist/runtime/src/config/explain.js +7 -0
  33. package/dist/runtime/src/config/hash.js +24 -0
  34. package/dist/runtime/src/config/merge.js +6 -2
  35. package/dist/runtime/src/config/migrate.js +19 -4
  36. package/dist/runtime/src/contracts.js +10 -1
  37. package/dist/runtime/src/fs/managed-block.js +35 -18
  38. package/dist/runtime/src/fs/manifest.js +32 -1
  39. package/dist/runtime/src/fs/transaction.js +14 -2
  40. package/dist/runtime/src/hooks/advisory.js +16 -0
  41. package/dist/runtime/src/hooks/codex-loop.js +439 -0
  42. package/dist/runtime/src/hooks/stop-service.js +70 -0
  43. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  44. package/dist/runtime/src/install/codex-loop.js +139 -0
  45. package/dist/runtime/src/install/doctor.js +182 -14
  46. package/dist/runtime/src/install/harness.js +294 -35
  47. package/dist/runtime/src/install/hooks.js +22 -17
  48. package/dist/runtime/src/install/ownership.js +146 -35
  49. package/dist/runtime/src/install/plan.js +276 -31
  50. package/dist/runtime/src/install/probes.js +9 -43
  51. package/dist/runtime/src/install/profiles.js +12 -3
  52. package/dist/runtime/src/install/surface-inspection.js +296 -0
  53. package/dist/runtime/src/install/surfaces.js +11 -0
  54. package/dist/runtime/src/install/uninstall.js +11 -4
  55. package/dist/runtime/src/install/update.js +13 -3
  56. package/dist/runtime/src/logging/local-log.js +25 -0
  57. package/dist/runtime/src/schema/validate.js +45 -19
  58. package/dist/runtime/src/task/service.js +3 -3
  59. package/dist/runtime/src/task/store.js +12 -3
  60. package/dist/runtime/src/verify/command-executor.js +113 -0
  61. package/dist/runtime/src/verify/evidence.js +4 -24
  62. package/dist/runtime/src/verify/service.js +20 -92
  63. package/dist/runtime/src/verify/spawn.js +6 -1
  64. package/docs/en/guides/configuration.md +159 -0
  65. package/docs/en/guides/quickstart.md +9 -0
  66. package/docs/en/guides/security.md +5 -0
  67. package/docs/en/spec/README.md +9 -0
  68. package/docs/en/spec/harness-adapters.md +104 -2
  69. package/docs/en/spec/maintenance.md +11 -0
  70. package/docs/en/spec/review.md +11 -0
  71. package/docs/zh-TW/guides/configuration.md +145 -0
  72. package/docs/zh-TW/guides/quickstart.md +9 -0
  73. package/docs/zh-TW/guides/security.md +5 -0
  74. package/docs/zh-TW/spec/README.md +9 -0
  75. package/docs/zh-TW/spec/harness-adapters.md +89 -3
  76. package/docs/zh-TW/spec/maintenance.md +11 -1
  77. package/docs/zh-TW/spec/review.md +10 -0
  78. package/package.json +4 -2
  79. package/schemas/config.schema.json +56 -2
  80. package/schemas/manifest.schema.json +19 -3
  81. package/templates/common/AGENTS.block.md +2 -1
  82. package/templates/common/CLAUDE.block.md +2 -1
  83. package/dist/runtime/src/review/claude-runner.js +0 -4
  84. package/dist/runtime/src/review/codex-runner.js +0 -4
@@ -0,0 +1,113 @@
1
+ import { runVerificationCommand } from "./spawn.js";
2
+ import { evaluateTestCount, parseTestCount } from "./test-count.js";
3
+ function classifyTestCountCode(code) {
4
+ const classes = {
5
+ TEST_COUNT_BELOW_MINIMUM: "test-count-below-minimum",
6
+ TEST_COUNT_INVALID: "test-count-invalid",
7
+ TEST_COUNT_OK: "none",
8
+ TEST_COUNT_REQUIREMENT_INVALID: "test-count-requirement-invalid",
9
+ TEST_COUNT_UNPARSEABLE: "test-count-unparseable",
10
+ ZERO_TESTS: "zero-tests"
11
+ };
12
+ return classes[code];
13
+ }
14
+ function untrustedResult(commandId) {
15
+ return {
16
+ commandId,
17
+ status: "UNKNOWN",
18
+ failureClass: "repository-untrusted",
19
+ exitCode: null,
20
+ signal: null,
21
+ timedOut: false,
22
+ durationMs: 0,
23
+ stdout: "",
24
+ stderr: "",
25
+ stdoutTruncated: false,
26
+ stderrTruncated: false
27
+ };
28
+ }
29
+ function classifyCommand(command, spawned) {
30
+ if (command.evidence.kind === "file") {
31
+ return {
32
+ status: spawned.status === "PASS" ? "UNKNOWN" : spawned.status,
33
+ failureClass: spawned.status === "PASS"
34
+ ? "file-evidence-unsupported"
35
+ : spawned.failureClass,
36
+ testCount: null
37
+ };
38
+ }
39
+ if (command.evidence.kind !== "test-count") {
40
+ return {
41
+ status: spawned.status,
42
+ failureClass: spawned.failureClass,
43
+ testCount: null
44
+ };
45
+ }
46
+ const testCount = parseTestCount(`${spawned.stdout}\n${spawned.stderr}`);
47
+ if (spawned.status !== "PASS") {
48
+ return {
49
+ status: spawned.status,
50
+ failureClass: spawned.failureClass,
51
+ testCount
52
+ };
53
+ }
54
+ const evaluation = evaluateTestCount(testCount, command.evidence.minimum);
55
+ return {
56
+ status: evaluation.status,
57
+ failureClass: classifyTestCountCode(evaluation.code),
58
+ testCount: evaluation.testCount
59
+ };
60
+ }
61
+ function diagnostic(spawned) {
62
+ return spawned.stderr || spawned.stdout || spawned.failureClass;
63
+ }
64
+ export async function executeConfiguredCommand(command, options) {
65
+ let spawned;
66
+ if (!options.trusted) {
67
+ spawned = untrustedResult(command.id);
68
+ }
69
+ else {
70
+ const runOptions = {
71
+ cwd: options.cwd,
72
+ ...(options.runner === undefined
73
+ ? {}
74
+ : { runner: options.runner }),
75
+ ...(options.env === undefined ? {} : { env: options.env }),
76
+ ...(options.now === undefined ? {} : { now: options.now }),
77
+ ...(options.outputLimitBytes === undefined
78
+ ? {}
79
+ : { outputLimitBytes: options.outputLimitBytes }),
80
+ ...(options.terminationGraceMs === undefined
81
+ ? {}
82
+ : { terminationGraceMs: options.terminationGraceMs })
83
+ };
84
+ spawned = await runVerificationCommand(command, runOptions);
85
+ }
86
+ const classified = classifyCommand(command, spawned);
87
+ return {
88
+ commandId: command.id,
89
+ required: command.required,
90
+ status: classified.status,
91
+ failureClass: classified.failureClass,
92
+ exitCode: spawned.exitCode,
93
+ signal: spawned.signal,
94
+ timedOut: spawned.timedOut,
95
+ testCount: classified.testCount,
96
+ diagnostic: diagnostic(spawned),
97
+ stdout: spawned.stdout,
98
+ stderr: spawned.stderr,
99
+ stdoutTruncated: spawned.stdoutTruncated,
100
+ stderrTruncated: spawned.stderrTruncated
101
+ };
102
+ }
103
+ export function aggregateVerificationStatus(results) {
104
+ const required = results.filter((result) => result.required);
105
+ const gating = required.length > 0 ? required : results;
106
+ if (gating.some((result) => result.status === "FAIL")) {
107
+ return "FAIL";
108
+ }
109
+ if (gating.some((result) => result.status === "UNKNOWN")) {
110
+ return "UNKNOWN";
111
+ }
112
+ return "PASS";
113
+ }
@@ -1,32 +1,12 @@
1
1
  import { join } from "node:path";
2
- import { SCHEMA_VERSION } from "../contracts.js";
2
+ import { EVIDENCE_SCHEMA_VERSION } from "../contracts.js";
3
3
  import { sha256 } from "../fs/hash.js";
4
4
  import { AgentOpsError } from "../fs/paths.js";
5
+ import { calculateConfigHash } from "../config/hash.js";
6
+ export { calculateConfigHash } from "../config/hash.js";
5
7
  import { validateEvidence } from "../schema/validate.js";
6
8
  import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
7
9
  import { redactSecrets } from "../security/redact.js";
8
- function canonicalJson(value) {
9
- if (value === null ||
10
- typeof value === "boolean" ||
11
- typeof value === "number" ||
12
- typeof value === "string") {
13
- return JSON.stringify(value);
14
- }
15
- if (Array.isArray(value)) {
16
- return `[${value.map(canonicalJson).join(",")}]`;
17
- }
18
- if (typeof value === "object") {
19
- const record = value;
20
- return `{${Object.keys(record)
21
- .sort()
22
- .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
23
- .join(",")}}`;
24
- }
25
- throw new AgentOpsError("CONFIG_HASH_INVALID", "Configuration contains an unsupported value.");
26
- }
27
- export function calculateConfigHash(config) {
28
- return sha256(canonicalJson(config));
29
- }
30
10
  function redactRecord(record) {
31
11
  return Object.fromEntries(Object.entries(record).map(([key, value]) => [
32
12
  redactSecrets(key),
@@ -43,7 +23,7 @@ function validateBuiltEvidence(value) {
43
23
  }
44
24
  export function buildVerificationEvidence(input) {
45
25
  return validateBuiltEvidence({
46
- schemaVersion: SCHEMA_VERSION,
26
+ schemaVersion: EVIDENCE_SCHEMA_VERSION,
47
27
  taskId: input.taskId,
48
28
  criterionId: input.criterionId,
49
29
  commandId: input.command.id,
@@ -3,55 +3,11 @@ import { validateTaskAgainstConfig } from "../schema/validate.js";
3
3
  import { collectChangeSurface } from "./change-surface.js";
4
4
  import { buildVerificationEvidence } from "./evidence.js";
5
5
  import { createFailureFingerprint } from "./fingerprint.js";
6
+ import { aggregateVerificationStatus, executeConfiguredCommand } from "./command-executor.js";
6
7
  import { selectVerificationScope } from "./scope.js";
7
- import { runVerificationCommand } from "./spawn.js";
8
- import { evaluateTestCount, parseTestCount } from "./test-count.js";
9
8
  function verificationError(code, message) {
10
9
  return new AgentOpsError(code, message);
11
10
  }
12
- function classifyTestCountCode(code) {
13
- const classes = {
14
- TEST_COUNT_BELOW_MINIMUM: "test-count-below-minimum",
15
- TEST_COUNT_INVALID: "test-count-invalid",
16
- TEST_COUNT_OK: "none",
17
- TEST_COUNT_REQUIREMENT_INVALID: "test-count-requirement-invalid",
18
- TEST_COUNT_UNPARSEABLE: "test-count-unparseable",
19
- ZERO_TESTS: "zero-tests"
20
- };
21
- return classes[code];
22
- }
23
- function classifyResult(command, spawned) {
24
- if (command.evidence.kind === "file") {
25
- return {
26
- status: spawned.status === "PASS" ? "UNKNOWN" : spawned.status,
27
- failureClass: spawned.status === "PASS"
28
- ? "file-evidence-unsupported"
29
- : spawned.failureClass,
30
- testCount: null
31
- };
32
- }
33
- if (command.evidence.kind !== "test-count") {
34
- return {
35
- status: spawned.status,
36
- failureClass: spawned.failureClass,
37
- testCount: null
38
- };
39
- }
40
- const testCount = parseTestCount(`${spawned.stdout}\n${spawned.stderr}`);
41
- if (spawned.status !== "PASS") {
42
- return {
43
- status: spawned.status,
44
- failureClass: spawned.failureClass,
45
- testCount
46
- };
47
- }
48
- const evaluation = evaluateTestCount(testCount, command.evidence.minimum);
49
- return {
50
- status: evaluation.status,
51
- failureClass: classifyTestCountCode(evaluation.code),
52
- testCount: evaluation.testCount
53
- };
54
- }
55
11
  function exitCategory(result) {
56
12
  if (result.timedOut) {
57
13
  return "timeout";
@@ -64,17 +20,6 @@ function exitCategory(result) {
64
20
  }
65
21
  return result.exitCode === 0 ? "exit-zero" : "nonzero-exit";
66
22
  }
67
- function overallStatus(results) {
68
- const required = results.filter((result) => result.required);
69
- const gating = required.length > 0 ? required : results;
70
- if (gating.some((result) => result.status === "FAIL")) {
71
- return "FAIL";
72
- }
73
- if (gating.some((result) => result.status === "UNKNOWN")) {
74
- return "UNKNOWN";
75
- }
76
- return "PASS";
77
- }
78
23
  function relevantCriteria(task, commandId) {
79
24
  return task.criteria.filter((criterion) => criterion.verifierIds.includes(commandId));
80
25
  }
@@ -85,9 +30,6 @@ function commandById(config, commandId) {
85
30
  }
86
31
  return command;
87
32
  }
88
- function diagnostics(spawned) {
89
- return spawned.stderr || spawned.stdout || spawned.failureClass;
90
- }
91
33
  export class VerificationService {
92
34
  #options;
93
35
  constructor(options) {
@@ -120,51 +62,37 @@ export class VerificationService {
120
62
  }
121
63
  async #runCommand(task, command) {
122
64
  const startedAt = (this.#options.now ?? (() => new Date().toISOString()))();
123
- const spawned = this.#options.trusted
124
- ? await runVerificationCommand(command, {
125
- cwd: await this.#commandCwd(command),
126
- runner: this.#options.processRunner
127
- })
128
- : {
129
- commandId: command.id,
130
- status: "UNKNOWN",
131
- failureClass: "repository-untrusted",
132
- exitCode: null,
133
- signal: null,
134
- timedOut: false,
135
- durationMs: 0,
136
- stdout: "",
137
- stderr: "",
138
- stdoutTruncated: false,
139
- stderrTruncated: false
140
- };
141
- const classified = classifyResult(command, spawned);
142
- const fingerprint = classified.status === "PASS"
65
+ const result = await executeConfiguredCommand(command, {
66
+ cwd: await this.#commandCwd(command),
67
+ runner: this.#options.processRunner,
68
+ trusted: this.#options.trusted
69
+ });
70
+ const fingerprint = result.status === "PASS"
143
71
  ? null
144
72
  : createFailureFingerprint({
145
73
  commandId: command.id,
146
- failureClass: classified.failureClass,
147
- exitCategory: spawned.timedOut
74
+ failureClass: result.failureClass,
75
+ exitCategory: result.timedOut
148
76
  ? "timeout"
149
- : spawned.signal !== null
77
+ : result.signal !== null
150
78
  ? "signal-exit"
151
- : spawned.exitCode === null
79
+ : result.exitCode === null
152
80
  ? "no-exit"
153
- : spawned.exitCode === 0
81
+ : result.exitCode === 0
154
82
  ? "exit-zero"
155
83
  : "nonzero-exit",
156
- diagnostics: diagnostics(spawned)
84
+ diagnostics: result.diagnostic
157
85
  });
158
86
  const finishedAt = (this.#options.now ?? (() => new Date().toISOString()))();
159
- const evidenceReferences = await this.#persistEvidence(task, command, startedAt, finishedAt, classified, spawned.exitCode);
87
+ const evidenceReferences = await this.#persistEvidence(task, command, startedAt, finishedAt, result, result.exitCode);
160
88
  return {
161
89
  commandId: command.id,
162
90
  required: command.required,
163
- status: classified.status,
164
- failureClass: classified.failureClass,
165
- exitCode: spawned.exitCode,
166
- timedOut: spawned.timedOut,
167
- testCount: classified.testCount,
91
+ status: result.status,
92
+ failureClass: result.failureClass,
93
+ exitCode: result.exitCode,
94
+ timedOut: result.timedOut,
95
+ testCount: result.testCount,
168
96
  diagnostic: fingerprint?.diagnostics ?? "",
169
97
  evidenceReferences
170
98
  };
@@ -185,7 +113,7 @@ export class VerificationService {
185
113
  for (const commandId of selection.verifierIds) {
186
114
  results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
187
115
  }
188
- const status = overallStatus(results);
116
+ const status = aggregateVerificationStatus(results);
189
117
  let signal = null;
190
118
  if (status === "PASS") {
191
119
  await this.#options.taskService.clearFailure(taskId);
@@ -138,6 +138,10 @@ export class NodeVerificationProcessRunner {
138
138
  const child = spawn(request.command, [...request.args], {
139
139
  cwd: request.cwd,
140
140
  detached: this.#platform !== "win32",
141
+ env: {
142
+ ...process.env,
143
+ ...(request.env ?? {})
144
+ },
141
145
  shell: request.shell,
142
146
  stdio: ["ignore", "pipe", "pipe"],
143
147
  windowsHide: true
@@ -258,7 +262,8 @@ export async function runVerificationCommand(command, options) {
258
262
  command: command.command,
259
263
  args: [...command.args],
260
264
  cwd: options.cwd,
261
- shell: command.shell === true
265
+ shell: command.shell === true,
266
+ ...(options.env === undefined ? {} : { env: options.env })
262
267
  });
263
268
  }
264
269
  catch {
@@ -0,0 +1,159 @@
1
+ # Configuration
2
+
3
+ Keep project configuration explicit and layered. Choose scope, harness, and profile deliberately; do not infer trust or security exceptions from `--yes`.
4
+
5
+ Use `--harness all` to select Codex, Claude Code, and opencode, or pass a
6
+ comma-separated subset such as `codex,opencode`. `both` remains an input alias
7
+ for the legacy Codex plus Claude selection.
8
+
9
+ Project Codex and opencode installations share the managed supplemental
10
+ `AGENTS.md` routing block and the `.agent-ops/AGENTS.md` rules artifact. The
11
+ block loads the managed baseline while project-specific instructions remain
12
+ authoritative. Claude uses the corresponding `CLAUDE.md` route and
13
+ `.agent-ops/CLAUDE.md` artifact. Opencode additionally gets
14
+ the agent-ops-owned `.opencode/plugins/agent-ops.js` file; `opencode.json` is
15
+ never modified. The plugin is generated with the installed absolute runtime
16
+ path, so update it through `agent-ops update` rather than editing it manually.
17
+
18
+ At user scope, Codex and opencode keep separate routing files under `.codex/`
19
+ and `.opencode/`; the global opencode plugin is placed under
20
+ `.config/opencode/plugins/`, or under `$XDG_CONFIG_HOME/opencode/plugins/`
21
+ when that variable points inside the managed user root. If OpenCode is
22
+ configured with `$OPENCODE_CONFIG_DIR`, the plugin is placed under its
23
+ `plugins/` directory instead. The installer discovers writable harness
24
+ surfaces and applies the selected target policy; use
25
+ `--hook-target <harness>=<surface-id>` when the managed default is not the
26
+ intended surface. Project-local Claude hooks use `.claude/settings.json` by
27
+ default; select `.claude/settings.local.json` explicitly when that is the
28
+ intended surface.
29
+ Advisory and guardrail hooks are registered only when the selected profile
30
+ implies them. Advisory runs through the real SessionStart path and is
31
+ fail-open. Claude and Codex lifecycle support is `supported`; OpenCode begins
32
+ at app initialization and is honestly reported as `degraded`.
33
+
34
+ ### Project-local loop profile
35
+
36
+ `--profile loop` is an opt-in project-scope profile. Select `codex`, `claude`,
37
+ or both (for example, `--harness codex,claude`); it requires a
38
+ POSIX-compatible `bash` and does not support Windows launchers yet. Start with
39
+ a dry run:
40
+
41
+ ```bash
42
+ agent-ops init --dry-run --scope project --harness codex,claude --profile loop --json
43
+ agent-ops init --scope project --harness codex,claude --profile loop --yes
44
+ ```
45
+
46
+ For each selected supported harness, agent-ops owns exactly one small launcher:
47
+ `.codex/hooks/agent-ops-loop.sh` or `.claude/hooks/agent-ops-loop.sh`. Both
48
+ launchers delegate to the same installed Node runtime, so they do not copy a
49
+ project-specific loop script. Codex also gets `.codex/config.toml` only when it
50
+ is absent. First installation seeds, without replacing existing content,
51
+ `loop-goal.md`, `loop-state.md`, and `loop-telemetry.jsonl` under the selected
52
+ harness directory. A hash-commented `.gitignore` block ignores those local
53
+ files.
54
+
55
+ The loop runs `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
56
+ `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`,
57
+ `SubagentStart`, and `SubagentStop`, but never adds `Stop`. It blocks only
58
+ high-confidence literal secrets in prompts or Bash commands, plus dangerous
59
+ Bash commands (including broad recursive deletion and `git reset --hard`). Codex uses its native
60
+ exit-code blocking mechanism; Claude Code receives its documented native JSON
61
+ decision shape. A `PermissionRequest`, including
62
+ `sandbox_permissions: "require_escalated"`, only records an outcome and emits
63
+ no allow or deny decision, preserving the host's normal approval flow.
64
+
65
+ Session context, telemetry, and compaction state are deliberately bounded.
66
+ Telemetry contains only timestamp, event, outcome, and rule identifier—not raw
67
+ prompts, commands, or credentials—and rotates by byte size. A pre-compaction
68
+ Git-status snapshot is redacted and written into a dedicated block in
69
+ `loop-state.md`, leaving surrounding user content intact. Installer update and
70
+ uninstall own only the launchers, native handler registrations, and exact
71
+ `.gitignore` block; goals, state, telemetry, and `config.toml` remain local
72
+ user files. If an existing `.codex/config.toml` explicitly says
73
+ `[features]` then `hooks = false`, planning stops with
74
+ `CODEX_LOOP_HOOKS_DISABLED` before any write.
75
+
76
+ Codex and Claude Code require their normal project-hook trust/review flow for
77
+ these generated handlers. The loop is a focused guardrail, not a complete
78
+ sandbox, permission bypass, or Stop-verification feature. See the [Codex hook
79
+ documentation](https://developers.openai.com/codex/config-advanced#hooks) and
80
+ the [Claude Code hook documentation](https://code.claude.com/docs/en/hooks)
81
+ before enabling it.
82
+
83
+ ### Runtime-failure safeguards
84
+
85
+ For the ordinary `guardrails` profile, `command-policy` is the only capability with a fail-closed failure mode. Claude
86
+ Code can emit its documented denial shape at native `PreToolUse` for a
87
+ classified invalid installed configuration. The managed OpenCode
88
+ `tool.execute.before` plugin can throw its documented command-policy denial or
89
+ unavailable-runtime error for its supported Bash surface. Codex is explicitly
90
+ non-enforcing (`unknown`). These are agent-ops output and plugin contracts, not
91
+ proof that a host honors a denial. `SessionStart` and `Stop` failure paths stay
92
+ fail-open for every adapter.
93
+
94
+ Claude's invalid-config fallback has four safeguards: (1) an absent project
95
+ configuration stays fail-open, so only an invalid `.agent-ops/config.json` can
96
+ reach the fallback; (2) the manifest must safely prove that the current harness
97
+ is installed; (3) a human can export `AGENT_OPS_DISABLE=1` in the shell before
98
+ launching the host to restore fail-open temporarily; and (4) a Claude Code
99
+ denial names the config path and tells the user to repair it or temporarily set
100
+ that shell variable. The variable is read only from the hook-process environment
101
+ and cannot be set in agent-ops configuration, a manifest, or managed files.
102
+
103
+ `guardrails` installs command policy but does not enable Stop verification. Stop
104
+ is a separate config-v2 feature and must be explicitly enabled with at least
105
+ one confirmed command:
106
+
107
+ ```json
108
+ {
109
+ "features": { "stopVerification": { "enabled": true } },
110
+ "verification": {
111
+ "commands": [
112
+ {
113
+ "id": "unit",
114
+ "command": "npm",
115
+ "args": ["test"],
116
+ "cwd": ".",
117
+ "required": true,
118
+ "evidence": { "kind": "test-count", "minimum": 1 }
119
+ }
120
+ ]
121
+ }
122
+ }
123
+ ```
124
+
125
+ Changing this feature changes native registration. Run:
126
+
127
+ ```bash
128
+ agent-ops update
129
+ agent-ops trust grant
130
+ ```
131
+
132
+ Without `update`, doctor can report `UPDATE_REQUIRED` for registration drift.
133
+ Separately, after a toolkit upgrade or effective profile or capability change
134
+ alters an intact path-independent managed rules artifact,
135
+ `artifact-staleness` reports `DEGRADED` with `UPDATE_REQUIRED`. `agent-ops
136
+ update` regenerates the artifact and clears that result; a missing or
137
+ hash-mismatched artifact remains an `artifacts` `FAIL`. Without the new trust
138
+ grant, trust-gated hooks remain stale. Stop is report-only: it continues the
139
+ harness for `PASS`, `FAIL`, or `UNKNOWN`, emits only bounded command ID, exit
140
+ code, test-count, config-hash, and timestamp evidence, and never completes a
141
+ task. Config v1 migrates deterministically to v2 with Stop disabled; old
142
+ binaries cannot read the migrated config, and the routing migration is
143
+ one-way after it is applied.
144
+
145
+ To narrow an existing installation, pass the desired list to `agent-ops update
146
+ --harness`; shared paths remain managed while removed harness-owned artifacts,
147
+ markers, and hooks are reconciled.
148
+
149
+ Installations using the previous canonical routing wording are migrated by
150
+ `agent-ops update`. If a managed block was edited, the command fails closed
151
+ until the change is reviewed.
152
+
153
+ Dry-run human and JSON plans omit raw harness settings content. They expose the
154
+ expected hash, content hash, and a safe summary while the internal apply plan
155
+ retains the complete merged settings. The manifest remains schema v2.
156
+
157
+ Use the [acceptance and evidence rules](../spec/acceptance-and-evidence.md)
158
+ when adding verification commands, and the [adapter rules](../spec/harness-adapters.md)
159
+ when configuring Codex, Claude Code, or opencode behavior.
@@ -0,0 +1,9 @@
1
+ # Quickstart
2
+
3
+ 1. Define 2–5 acceptance criteria.
4
+ 2. Inspect the smallest relevant scope.
5
+ 3. Run a failing test before implementation.
6
+ 4. Implement the smallest change, then run typecheck and tests.
7
+ 5. Record evidence and request independent review before handoff.
8
+
9
+ See the [normative loop](../spec/loop-engineering.md) and [review rules](../spec/review.md).
@@ -0,0 +1,5 @@
1
+ # Security
2
+
3
+ Treat commands, logs, configuration, and external input as untrusted. Redact credentials, block destructive commands by default, and keep local state out of tracked project paths.
4
+
5
+ Read the [guardrails](../spec/guardrails.md), [judgment rules](../spec/judgment.md), and [maintenance rules](../spec/maintenance.md) before changing policy or hook files.
@@ -2,6 +2,15 @@
2
2
 
3
3
  This is the normative English specification for bounded, evidence-driven work.
4
4
 
5
+ The harness adapter rules cover Codex, Claude Code, and opencode. The opencode
6
+ integration is a generated local plugin; it does not manage `opencode.json`.
7
+
8
+ Configuration is versioned independently from the manifest. Config v1 migrates
9
+ to config v2 with Stop verification disabled; changing the capability requires
10
+ `agent-ops update` followed by `agent-ops trust grant`. Stop verification is
11
+ explicit, trusted, report-only, and never completes a task. Dry-run plans keep
12
+ foreign settings opaque, and the routing migration is one-way once applied.
13
+
5
14
  - [Loop engineering](./loop-engineering.md)
6
15
  - [Acceptance and evidence](./acceptance-and-evidence.md)
7
16
  - [Judgment](./judgment.md)
@@ -1,12 +1,22 @@
1
1
  # Harness Adapters
2
2
 
3
+ OpenCode plugin behavior in this document was checked against the [official
4
+ plugin documentation](https://opencode.ai/docs/plugins/) and [Bun shell
5
+ documentation](https://bun.sh/docs/runtime/shell) on 2026-07-31. Codex and
6
+ Claude Code loop-hook behavior was checked against their [Codex hook
7
+ documentation](https://developers.openai.com/codex/config-advanced#hooks) and
8
+ [Claude Code hook documentation](https://code.claude.com/docs/en/hooks) on
9
+ 2026-08-03. Revalidate: when any vendor reference changes.
10
+
3
11
  ## HARNESS-ADAPTER-001
4
12
 
5
- An adapter MUST preserve native harness semantics and MUST label unsupported behavior as UNKNOWN.
13
+ An adapter MUST preserve native harness semantics and MUST declare each
14
+ capability as supported, degraded, unsupported, or unknown.
6
15
 
7
16
  - Trigger: Mapping portable lifecycle or review behavior into a native harness.
8
17
  - Action: Keep ownership narrow, retain user configuration, and document limitations.
9
- - Evidence: Adapter tests cover existing configuration and unsupported outcomes.
18
+ - Evidence: Adapter tests cover existing configuration, support declarations,
19
+ and native failure outcomes.
10
20
  - Positive: `A Codex blocking outcome remains UNKNOWN when native denial is unconfirmed.`
11
21
  - Negative: `Assume Claude exit semantics apply to Codex.`
12
22
 
@@ -19,3 +29,95 @@ An adapter MUST be idempotent and MUST avoid deleting user-owned handlers.
19
29
  - Evidence: Existing configuration fixtures remain intact after apply and uninstall.
20
30
  - Positive: `Managed handler updates while unrelated handlers remain byte-for-byte present.`
21
31
  - Negative: `Replace the complete settings file with toolkit defaults.`
32
+
33
+ ## HARNESS-ADAPTER-003
34
+
35
+ A file-backed adapter MUST register only hooks implied by the active
36
+ capabilities and MUST track generated source as one whole-file artifact.
37
+
38
+ - Trigger: Installing or probing a harness whose extension point is a plugin file.
39
+ - Action: For opencode, manage `.opencode/plugins/agent-ops.js` in a project or `.config/opencode/plugins/agent-ops.js` at user scope (`$XDG_CONFIG_HOME/opencode/plugins/agent-ops.js` when configured inside the managed user root, or `$OPENCODE_CONFIG_DIR/plugins/agent-ops.js` when that native config directory is configured), leave `opencode.json` untouched, and deduplicate a project `AGENTS.md` contribution by path.
40
+ - Evidence: The manifest contains the plugin hash, the generated source contains only the selected hook registrations, and shared project markers occur once.
41
+ - Positive: `codex,opencode` produces one project AGENTS route and one hashed opencode plugin.
42
+ - Negative: `Add an opencode.json instructions entry or register a plugin for a core-only profile.`
43
+
44
+ ## HARNESS-ADAPTER-004
45
+
46
+ The opencode shim MUST invoke the absolute runtime path from the selected
47
+ project directory, MUST fail open for
48
+ advisory events, and MUST throw its documented command-policy error when the
49
+ runtime is unavailable.
50
+
51
+ - Trigger: The generated plugin invokes `agent-ops` or receives an invalid runtime decision.
52
+ - Action: Keep normalization and native output encoding in the runtime adapter,
53
+ throw its documented policy reason for a deny decision, and run
54
+ lifecycle-summary through the shared advisory implementation. App-scoped
55
+ plugin initialization remains degraded for per-session lifecycle fidelity.
56
+ - Evidence: Shim import tests cover allow, deny, and missing-runtime behavior;
57
+ denial fixtures assert output shape only; doctor reports OpenCode lifecycle
58
+ support as `DEGRADED`.
59
+ - Positive: `When the runtime is unavailable, SessionStart stays fail-open and the generated plugin throws its documented command-policy error for a Bash pre-tool hook.`
60
+ - Negative: `Fall back to a PATH-resolved agent-ops executable, claim an OpenCode host honors a thrown denial, or claim app initialization is a per-session Stop-equivalent.`
61
+
62
+ ## HARNESS-ADAPTER-005
63
+
64
+ Each descriptor MUST expose separate control and runtime adapters. The control
65
+ adapter owns installation planning, routing, ownership, probes, and the
66
+ in-memory capability registration matrix. The runtime adapter owns native input
67
+ decoding, normalized events, native output encoding, and runtime-failure output.
68
+
69
+ - Trigger: Adding a harness surface or a generic capability.
70
+ - Action: Add the capability-to-native registration to the owning harness,
71
+ including its support level and runtime-failure mode; do not add native
72
+ events to a universal union.
73
+ - Evidence: Every declared `supported` registration is exercised through the
74
+ real CLI hook process; denial-shape fixtures assert documented wire shapes,
75
+ not host runtime enforcement; unsupported Stop/lifecycle registrations are
76
+ not reported as enforcement success.
77
+ - Positive: `A fail-closed Claude command-policy runtime failure produces the documented PreToolUse denial shape through runHookCommand.`
78
+ - Negative: `Mark SessionStart supported while dispatchHookEvent has no advisory implementation.`
79
+
80
+ ## HARNESS-ADAPTER-006
81
+
82
+ The project-local `loop` profile MUST be opt-in, project scoped, and use one
83
+ shared runtime behind minimal Codex and Claude Code launchers. It MUST NOT copy
84
+ policy into project-specific scripts or alter an ordinary permission request.
85
+
86
+ - Trigger: A project selects `loop` with Codex, Claude Code, or both.
87
+ - Action: Generate only the selected `.codex/hooks/agent-ops-loop.sh` and/or
88
+ `.claude/hooks/agent-ops-loop.sh` launchers, register the documented loop
89
+ lifecycle events except `Stop`, and preserve foreign hook groups. Block only
90
+ high-confidence literal credentials at `UserPromptSubmit` or Bash
91
+ `PreToolUse`, and dangerous Bash commands at `PreToolUse`, using the documented native denial shape. Emit no
92
+ decision for `PermissionRequest`, including escalated permissions.
93
+ - Evidence: Install-plan, loop-runtime, update, uninstall, and doctor tests
94
+ cover generated paths, Codex/Claude wire output, privacy bounds,
95
+ configuration conflict handling, state preservation, and registration drift.
96
+ - Positive: `A Claude PreToolUse dangerous Bash command receives a native deny while a PermissionRequest produces no allow or deny decision.`
97
+ - Negative: `Copy a project loop policy into both shell launchers, auto-approve sandbox escalation, or add a loop Stop handler.`
98
+
99
+ The current registration matrix is intentionally asymmetric:
100
+
101
+ | Capability | Codex | Claude Code | OpenCode |
102
+ | --- | --- | --- | --- |
103
+ | lifecycle-summary | supported | supported | degraded |
104
+ | command-policy | unknown | supported | supported |
105
+ | optional-stop-verify | unsupported | supported | degraded |
106
+
107
+ For runtime-failure handling, only `command-policy` is fail-closed. Claude
108
+ Code can emit its documented `PreToolUse` denial shape for a classified invalid
109
+ installed configuration; the managed OpenCode `tool.execute.before` plugin can
110
+ throw its documented denial or unavailable-runtime error for its supported Bash
111
+ surface. Codex remains `unknown` and never emits a denial. Fixture tests assert
112
+ these wire and plugin shapes only; they do not prove that a host honors a
113
+ denial. Every `SessionStart` and `Stop` failure path remains fail-open.
114
+
115
+ Stop verification is explicit, trusted, report-only, and disabled by default.
116
+ Every Stop result continues the native harness and may carry only bounded
117
+ command evidence; it is never task-completion evidence.
118
+
119
+ The `loop` profile is separate from the ordinary capability matrix above. It
120
+ stores only bounded local event metadata, returns bounded redacted session
121
+ context, and preserves local goal, state, telemetry, and Codex TOML files on
122
+ update or uninstall. A clearly parsed `[features]` / `hooks = false` in an
123
+ existing Codex configuration MUST reject loop planning before any write.
@@ -19,3 +19,14 @@ Volatile harness facts MUST live in adapter or research documentation and MUST i
19
19
  - Evidence: The document has a source and `Revalidate:` condition.
20
20
  - Positive: `Revalidate: when the vendor hook reference changes.`
21
21
  - Negative: `Treat a transient CLI flag as a permanent core rule.`
22
+
23
+ ## MAINTAIN-PLUGIN-001
24
+
25
+ Generated harness plugins MUST be treated as managed artifacts and MUST NOT be
26
+ hand-edited in place.
27
+
28
+ - Trigger: Updating the runtime, capabilities, or a vendor plugin contract.
29
+ - Action: Regenerate the opencode shim, verify its manifest hash, and revalidate the vendor behavior before release.
30
+ - Evidence: The artifact hash and shim import tests pass; release documentation records the revalidation condition.
31
+ - Positive: `agent-ops update` rewrites a changed plugin after ownership checks pass.
32
+ - Negative: `Patch the opencode plugin manually and retain the old manifest hash.`