@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5

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 (101) hide show
  1. package/core/.antigravity-plugin/plugin.json +0 -1
  2. package/core/.claude-plugin/plugin.json +1 -1
  3. package/core/.codex-plugin/plugin.json +1 -1
  4. package/core/.plugin/plugin.json +1 -1
  5. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  6. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  8. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  9. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  10. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  11. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  12. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  13. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  15. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  16. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  17. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  18. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  19. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  20. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  22. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  23. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  24. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  26. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  27. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  28. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  29. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  30. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  31. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  32. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  33. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  34. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  35. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  36. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  37. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  38. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  39. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  40. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  41. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  42. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  43. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  44. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  45. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  46. package/core/registry/assets.json +19 -0
  47. package/core/registry/contracts.json +2 -2
  48. package/core/registry/harnesses.json +196 -10
  49. package/core/registry/lifecycle.json +2626 -7
  50. package/core/registry/storage.json +24 -1
  51. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  52. package/core/shared/skills/do-design/SKILL.md +6 -6
  53. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  54. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  55. package/core/shared/skills/do-implement/SKILL.md +3 -2
  56. package/core/shared/skills/do-plan/SKILL.md +7 -8
  57. package/package.json +1 -1
  58. package/src/adapters/antigravity/index.js +83 -47
  59. package/src/adapters/claude/index.js +15 -1
  60. package/src/adapters/codex/hooks.js +40 -5
  61. package/src/adapters/codex/index.js +52 -9
  62. package/src/adapters/copy-tree.js +4 -4
  63. package/src/adapters/gemini/hooks.js +57 -16
  64. package/src/adapters/gemini/index.js +57 -18
  65. package/src/adapters/kiro/index.js +15 -1
  66. package/src/adapters/pi/hooks.js +119 -0
  67. package/src/adapters/pi/index.js +34 -9
  68. package/src/cli/commands/install.js +1 -1
  69. package/src/cli/commands/update.js +1 -1
  70. package/src/cli/index.js +17 -5
  71. package/src/cli/runtime-commands.js +51 -12
  72. package/src/lifecycle/coverage.js +57 -0
  73. package/src/lifecycle/hook-step.js +61 -0
  74. package/src/lifecycle/index.js +28 -14
  75. package/src/registry/index.js +136 -7
  76. package/src/runtime/candidate.js +505 -0
  77. package/src/runtime/claims.js +14 -5
  78. package/src/runtime/cli-result.js +11 -4
  79. package/src/runtime/cli.js +35 -268
  80. package/src/runtime/context/budget.js +203 -0
  81. package/src/runtime/context/compose.js +1367 -0
  82. package/src/runtime/context/delivery-store.js +102 -0
  83. package/src/runtime/context/mandatory.js +594 -0
  84. package/src/runtime/context/pointer.js +142 -0
  85. package/src/runtime/context-pack.js +1029 -244
  86. package/src/runtime/evidence-fields.js +39 -0
  87. package/src/runtime/evidence-ledger.js +7 -0
  88. package/src/runtime/freshness.js +44 -7
  89. package/src/runtime/outcome.js +1 -1
  90. package/src/runtime/readiness.js +83 -13
  91. package/src/runtime/retrieval-plan.js +422 -73
  92. package/src/runtime/retrieval-record.js +223 -0
  93. package/src/runtime/scaffold/artifacts.js +3 -0
  94. package/src/runtime/source-version.js +289 -0
  95. package/src/runtime/task-attempts.js +1 -1
  96. package/src/runtime/task-scope.js +28 -3
  97. package/src/runtime/task-state.js +72 -7
  98. package/core/.antigravity-plugin/hooks.json +0 -39
  99. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  100. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  101. package/core/harnesses/shared/hooks/hooks.json +0 -47
@@ -0,0 +1,159 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // C.3's renderer replaces this relative source-tree path for each install scope.
6
+ const BRIDGE = new URL('../../shared/hooks/bridge/bridge.sh', import.meta.url);
7
+ const ROUTES = [
8
+ { tools: ['edit', 'write'], policy: 'pre-implementation-gate' },
9
+ { tools: ['bash'], policy: 'pre-bash-guard' },
10
+ ];
11
+ const LIMIT = 1024 * 1024;
12
+ const TIMEOUT_MS = 5000;
13
+ const ERROR_REASON = (policy) => `[${policy}] DoFlow policy error; details: doflow-run failure list`;
14
+
15
+ function route(toolName) {
16
+ for (const row of ROUTES) {
17
+ if (row.tools.includes(toolName)) return row.policy;
18
+ }
19
+ return typeof toolName === 'string' && toolName.startsWith('mcp__') ? 'mcp-tool-guard' : undefined;
20
+ }
21
+
22
+ function captureFailure(policy, point) {
23
+ const script = new URL('../policies/capture-failure.sh', BRIDGE);
24
+ try {
25
+ const child = spawn('bash', ['-c', '. "$1"; doflow_capture_failure "$2" "$3"', 'doflow-capture', fileURLToPath(script), policy, point], {
26
+ detached: true,
27
+ stdio: 'ignore',
28
+ });
29
+ child.unref();
30
+ } catch {
31
+ // Failure capture must never change Pi's tool-call decision.
32
+ }
33
+ }
34
+
35
+ function bridgeCall(policy, payload, ctx) {
36
+ return new Promise((resolve, reject) => {
37
+ let child;
38
+ try {
39
+ const env = { ...process.env };
40
+ delete env.DOFLOW_PROJECT_DIR;
41
+ child = spawn('bash', [fileURLToPath(BRIDGE), 'pi', 'tool_call', policy], {
42
+ cwd: ctx.cwd,
43
+ env,
44
+ stdio: ['pipe', 'pipe', 'pipe'],
45
+ detached: true,
46
+ });
47
+ } catch (error) {
48
+ reject(Object.assign(error, { failurePoint: 'bridge-spawn' }));
49
+ return;
50
+ }
51
+
52
+ const stdout = [];
53
+ let stdoutBytes = 0;
54
+ let stderrBytes = 0;
55
+ let settled = false;
56
+ let timedOut = false;
57
+ let overflow = false;
58
+ let aborted = false;
59
+ const finish = (error, result) => {
60
+ if (settled) return;
61
+ settled = true;
62
+ clearTimeout(timer);
63
+ ctx.signal?.removeEventListener('abort', onAbort);
64
+ if (error) reject(error);
65
+ else resolve(result);
66
+ };
67
+ const kill = () => {
68
+ try {
69
+ if (child.pid && process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL');
70
+ else child.kill('SIGKILL');
71
+ } catch {
72
+ try { child.kill('SIGKILL'); } catch {}
73
+ }
74
+ };
75
+ const fail = (point) => {
76
+ overflow = point === 'bridge-output-limit';
77
+ kill();
78
+ finish(Object.assign(new Error(point), { failurePoint: point }));
79
+ };
80
+ const onAbort = () => {
81
+ aborted = true;
82
+ kill();
83
+ finish(null, undefined);
84
+ };
85
+ const timer = setTimeout(() => {
86
+ timedOut = true;
87
+ kill();
88
+ }, TIMEOUT_MS);
89
+ if (ctx.signal?.aborted) onAbort();
90
+ else ctx.signal?.addEventListener('abort', onAbort, { once: true });
91
+
92
+ child.on('error', (error) => finish(Object.assign(error, { failurePoint: 'bridge-spawn' })));
93
+ child.stdout.on('data', (chunk) => {
94
+ stdoutBytes += chunk.length;
95
+ if (stdoutBytes > LIMIT) return fail('bridge-output-limit');
96
+ stdout.push(chunk);
97
+ });
98
+ child.stderr.on('data', (chunk) => {
99
+ stderrBytes += chunk.length;
100
+ if (stderrBytes > LIMIT) return fail('bridge-output-limit');
101
+ });
102
+ child.on('close', (code) => {
103
+ if (settled) return;
104
+ if (aborted) return finish(null, undefined);
105
+ if (timedOut) return finish(Object.assign(new Error('bridge timed out'), { failurePoint: 'bridge-timeout' }));
106
+ if (overflow) return finish(Object.assign(new Error('bridge output exceeded limit'), { failurePoint: 'bridge-output-limit' }));
107
+ if (code !== 0) return finish(Object.assign(new Error(`bridge exited ${code}`), { failurePoint: 'bridge-spawn' }));
108
+ const output = Buffer.concat(stdout).toString('utf8');
109
+ if (!output.trim()) return finish(null, undefined);
110
+ if (output.trim().split(/\r?\n/).length !== 1) {
111
+ return finish(Object.assign(new Error('bridge answer must be one JSON line'), { failurePoint: 'bridge-spawn' }));
112
+ }
113
+ try {
114
+ const answer = JSON.parse(output);
115
+ if (!answer || typeof answer !== 'object' || Array.isArray(answer) || answer.block !== true || typeof answer.reason !== 'string') {
116
+ throw new Error('invalid bridge answer');
117
+ }
118
+ finish(null, { block: true, reason: answer.reason });
119
+ } catch {
120
+ finish(Object.assign(new Error('invalid bridge answer'), { failurePoint: 'bridge-spawn' }));
121
+ }
122
+ });
123
+ child.stdin.on('error', () => {});
124
+ child.stdin.end(payload);
125
+ });
126
+ }
127
+
128
+ export default function (pi) {
129
+ pi.on('tool_call', async (event, ctx) => {
130
+ let policy = 'unknown';
131
+ let profile = process.env.DOFLOW_FAULT_PROFILE || 'compat-1';
132
+ try {
133
+ policy = route(event.toolName);
134
+ if (!policy) return undefined;
135
+ profile = process.env.DOFLOW_FAULT_PROFILE || 'compat-1';
136
+ if (profile !== 'compat-1' && profile !== 'strict-1') captureFailure(policy, 'fault-profile-unknown');
137
+ if (!existsSync(BRIDGE)) {
138
+ captureFailure(policy, 'bridge-missing');
139
+ return { block: true, reason: '[doflow] shared hooks lack the bridge; run: doflow update -t pi' };
140
+ }
141
+ const payload = JSON.stringify({
142
+ type: event.type,
143
+ toolName: event.toolName,
144
+ toolCallId: event.toolCallId,
145
+ parentToolCallId: event.parentToolCallId,
146
+ input: event.input,
147
+ cwd: ctx.cwd,
148
+ sessionId: ctx.sessionManager?.getSessionId?.() ?? null,
149
+ });
150
+ if (Buffer.byteLength(payload) > LIMIT) throw Object.assign(new Error('bridge input exceeded limit'), { failurePoint: 'bridge-input-limit' });
151
+ return await bridgeCall(policy, payload, ctx);
152
+ } catch (error) {
153
+ const point = error?.failurePoint || 'bridge-spawn';
154
+ captureFailure(policy, point === 'bridge-timeout' || point === 'bridge-missing' ? point : 'bridge-spawn');
155
+ if (profile === 'strict-1') return { block: true, reason: ERROR_REASON(policy) };
156
+ return undefined;
157
+ }
158
+ });
159
+ }
@@ -1,16 +1,17 @@
1
1
  # The normalized hook envelope, and each front door's share of the migration
2
2
 
3
+ Every shipped bridge places envelope version 1 under `doflow_event`, beside the native fields. Policies read v1 fields only when `version` is the JSON number `1`; any other version is unsupported and is evaluated as such (IC-034, DEC-030).
4
+
3
5
  `policies/pre-implementation-gate.sh` prefers one normalized event over every native payload
4
6
  shape (review R7):
5
7
 
6
8
  ```json
7
- {"doflow_event": {"operation": "edit", "paths": ["src/a.js"], "projectRoot": "/repo", "taskId": "001-auth"}}
9
+ {"doflow_event": {"version": 1, "harness": "claude", "event": "PreToolUse", "operation": "edit", "paths": ["src/a.js"], "projectRoot": "/repo", "taskId": "001-auth"}}
8
10
  ```
9
11
 
10
- A front door that sends this envelope needs none of the policy's legacy union parsing — the four
11
- tool-name spellings, five file-path field names, and Codex `apply_patch` diff extraction exist
12
- only for front doors still forwarding raw native payloads. Each adapter that adopts the envelope
13
- retires its share of that union; when the last one does, the union decoder is deleted.
12
+ The envelope is the only policy input contract. A caller outside a shipped bridge must either
13
+ send a v1 envelope itself or pipe its native payload through
14
+ `bash .doflow/shared/hooks/bridge/bridge.sh <harness> <event> <policy>`.
14
15
 
15
16
  ## Which native event each front door decodes
16
17
 
@@ -24,16 +25,31 @@ tool's official docs (cross-checked against tool source where docs were incomple
24
25
  | Gemini CLI | `BeforeTool` | also offers `BeforeToolSelection` (pre-selection restriction), which the gate does not use |
25
26
  | GitHub Copilot | `preToolUse` | camelCase event names; `permissionRequest` is CLI-only, not fired in the cloud agent |
26
27
  | OpenCode | `tool.execute.before` | a pluggable hook method, not a sub-event of the generic `event` hook |
27
- | Pi | `tool_execution_start` | Pi's 36-event surface also exposes `tool_call`; the execution-start event is the gate's anchor |
28
+ | Pi | `tool_call` | The live-observed `cli-extension` pre-implementation gate row blocks. Project-interactive and user-interactive rows remain unknown; interactive loading and project-trust behavior were not observed. `tool_execution_start` is observation-only |
28
29
  | Antigravity | `PreToolUse` | 5-event surface, `hooks.json` + stdin/stdout JSON contract |
29
30
 
31
+ Pi 1.1.0 declares 41 `on()` overloads in its shipped extension types. `tool_call` is the enforcement event; `tool_execution_start` is not a gate.
32
+
30
33
  `operation` is `"edit"` for anything that writes source; a decoder maps its native create/replace/
31
34
  patch tool vocabulary down to that one word. Non-edit operations may be sent (the policy allows
32
35
  them through), but sending nothing for a non-edit tool is equally correct and cheaper.
33
36
 
34
- ## Rules that hold on both paths
35
-
36
- - The envelope, when present, **outranks** any native fields beside it.
37
+ ## Version and mixed-install rules
38
+
39
+ - A missing envelope or version, empty or non-object input, or a missing `contract.sh` is out of
40
+ step. The gate, bash guard and MCP guard deny with `DoFlow hooks out of step; run: doflow update
41
+ -t <harness>`; Stop and context policies exit silently, except the prompt hook emits its `{}`
42
+ no-op. Other envelope versions are unsupported and policies evaluate nothing.
43
+ - If a door cannot load `bridge/bridge.sh`, it renders its fixed native out-of-step response. The
44
+ Pi extension blocks with the fixed native out-of-step reason; a present bridge that fails to spawn or answer follows
45
+ the configured bridge-fault behavior.
46
+
47
+ Other current bridge behavior: Codex Stop crash stderr is suppressed (DEC-037). Stop-check
48
+ enforcement is answered from Stop coverage rows (DEC-038). Strict-1 never fault-blocks Stop, though
49
+ a real Stop deny still blocks (DEC-039). Bridge capture uses no temporary file, so an unwritable
50
+ `TMPDIR` does not turn a gate deny into an allow; background jobs do not hold Stop or SessionStart
51
+ open; strict-1 renders native denies without `jq`. `doflow update -t codex -f` re-merges edited
52
+ Codex `hooks.json` while keeping user entries (DEC-040).
37
53
  - Root resolution: `DOFLOW_PROJECT_DIR` beats the envelope's `projectRoot` beats git discovery.
38
- - The policy remains a pure file-existence gate (FR-012): the envelope changes how it hears about
39
- an edit, never what it decides, and it must never consult readiness or evidence state.
54
+ - The pre-implementation gate checks required artifacts and reads run and readiness records; it
55
+ does not consult evidence state.
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Antigravity payload/decision adapter for the Cross-Harness Hook Runner (design.md C2's
5
- * Payload Adapter / Decision Adapter seam, D2).
4
+ * Antigravity native decision renderer for the Cross-Harness Hook Runner's
5
+ * Decision Adapter seam, D2). The shared bridge owns native payload decoding.
6
6
  *
7
7
  * Antigravity's own native contracts — evidenced by core/harnesses/antigravity/hooks/*.sh (the
8
8
  * shims src/adapters/antigravity/index.js actually projects today) and
@@ -19,67 +19,24 @@
19
19
  * vocabulary than PreToolUse's allow/deny/ask/force_ask, not an oversight.
20
20
  *
21
21
  * SessionStart and PostToolUse have no dedicated Antigravity-native shim in
22
- * src/adapters/antigravity/index.js's live projection; core/.antigravity-plugin/hooks.json (a
23
- * separate, read-only-to-this-task artifact) documents both routed directly to the *Gemini*
24
- * session-start.sh / post-edit-lint.sh scripts, which read the Claude/Gemini-style
25
- * session_id/cwd/tool_name/tool_input field names, not the toolCall envelope above — this
26
- * adapter honors that evidence for those two events rather than guessing at a toolCall shape
27
- * with no corroborating source. No Phase A canonical policy owns PostToolUse today (only
22
+ * src/adapters/antigravity/index.js's live projection. No Phase A canonical policy owns PostToolUse today (only
28
23
  * session-context, pre-implementation-gate, mcp-tool-guard, stop-check, pre-bash-guard exist),
29
24
  * so its native shape is an ack passthrough either way.
30
25
  */
31
26
 
32
- const { FILE_PATH_ARG_FIELDS, COMMAND_ARG_FIELDS, firstArgField } = require('./arg-fields');
33
-
34
- function toCanonical(event, payload) {
35
- const safePayload = payload && typeof payload === 'object' ? payload : {};
36
-
37
- switch (event) {
38
- case 'SessionStart':
39
- return { session_id: safePayload.session_id || '', cwd: safePayload.cwd || '' };
40
-
41
- case 'PreToolUse': {
42
- const toolCall = safePayload.toolCall || {};
43
- const args = toolCall.args || {};
44
- return {
45
- tool_name: toolCall.name || '',
46
- tool_input: {
47
- file_path: firstArgField(args, FILE_PATH_ARG_FIELDS),
48
- command: firstArgField(args, COMMAND_ARG_FIELDS),
49
- },
50
- };
51
- }
52
-
53
- case 'PostToolUse':
54
- // No Phase A policy consumes this event — canonical shape kept for symmetry only.
55
- return {
56
- tool_name: safePayload.tool_name || '',
57
- session_id: safePayload.session_id || '',
58
- tool_input: { file_path: (safePayload.tool_input && safePayload.tool_input.file_path) || '' },
59
- };
60
-
61
- case 'Stop':
62
- return {
63
- session_id: safePayload.session_id || '',
64
- transcript_path: safePayload.transcriptPath || safePayload.transcript_path || '',
65
- };
66
-
67
- case 'PreInvocation':
68
- // No Phase A policy — ack only (injectSteps handled entirely in toNative).
69
- return {};
70
-
71
- default:
72
- return safePayload;
73
- }
74
- }
75
-
76
27
  function toNative(event, decision) {
77
28
  const safeDecision = decision || { decision: 'allow' };
29
+ const blocked = safeDecision.effect === 'block' || safeDecision.decision === 'deny';
30
+ const reason = safeDecision.outcome === 'deny'
31
+ ? safeDecision.reason
32
+ : safeDecision.effect === 'block'
33
+ ? `[${safeDecision.policy}] DoFlow policy error; details: doflow-run failure list`
34
+ : safeDecision.reason;
78
35
 
79
36
  switch (event) {
80
37
  case 'PreToolUse':
81
- if (safeDecision.decision === 'deny') {
82
- return { decision: 'deny', reason: safeDecision.reason };
38
+ if (blocked) {
39
+ return { decision: 'deny', reason };
83
40
  }
84
41
  return { decision: 'allow' };
85
42
 
@@ -88,8 +45,8 @@ function toNative(event, decision) {
88
45
  return {};
89
46
 
90
47
  case 'Stop':
91
- if (safeDecision.decision === 'deny') {
92
- return { decision: 'continue', reason: safeDecision.reason };
48
+ if (blocked) {
49
+ return { decision: 'continue', reason };
93
50
  }
94
51
  // Silence (no positive field) lets the session stop, per Antigravity's own documented
95
52
  // Stop contract — not the allow/deny vocabulary PreToolUse uses.
@@ -108,4 +65,4 @@ function toNative(event, decision) {
108
65
  }
109
66
  }
110
67
 
111
- module.exports = { toCanonical, toNative };
68
+ module.exports = { toNative };
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # Shared lifecycle bridge. Sourcing defines functions only; it sets no shell options or traps.
3
+ case ${BASH_SOURCE[0]} in */*) _DOFLOW_BRIDGE_DIR=${BASH_SOURCE[0]%/*} ;; *) _DOFLOW_BRIDGE_DIR=. ;; esac
4
+
5
+ doflow_bridge_failure() {
6
+ local policy="$1" point="$2"
7
+ ( . "$_DOFLOW_BRIDGE_DIR/../policies/capture-failure.sh"; doflow_capture_failure "$policy" "$point" ) >/dev/null 2>&1 || true
8
+ }
9
+
10
+ doflow_bridge_classify() {
11
+ local code="$1" record="$2" outcome="" decision_code="" version=0 reason="$3"
12
+ if [ -n "$record" ]; then
13
+ outcome="${record#*\"outcome\":\"}"; outcome="${outcome%%\"*}"
14
+ decision_code="${record#*\"code\":\"}"; decision_code="${decision_code%%\"*}"
15
+ version="${record#*\"policyVersion\":}"; version="${version%%,*}"
16
+ case "$version" in ''|*[^0-9]*) version=0;; esac
17
+ fi
18
+ DF_BRIDGE_OUTCOME=error; DF_BRIDGE_CODE=exit-"$code"; DF_BRIDGE_VERSION="$version"; DF_BRIDGE_REASON="$DF_BRIDGE_CODE"; DF_BRIDGE_POINT="eval-error:exit-$code"
19
+ if [ "$code" -eq 2 ] 2>/dev/null; then
20
+ if [ "$outcome" = deny ]; then DF_BRIDGE_OUTCOME=deny; DF_BRIDGE_CODE="$decision_code"; DF_BRIDGE_REASON="$reason"; DF_BRIDGE_POINT=""
21
+ else DF_BRIDGE_CODE=exit-2-unmarked; DF_BRIDGE_REASON=exit-2-unmarked; DF_BRIDGE_POINT=eval-error:exit-2-unmarked; fi
22
+ elif [ "$code" -eq 0 ] 2>/dev/null; then
23
+ case "$outcome" in
24
+ allow|advisory|error|unsupported) DF_BRIDGE_OUTCOME="$outcome"; DF_BRIDGE_CODE="$decision_code"; DF_BRIDGE_REASON="$decision_code"; DF_BRIDGE_POINT="" ;;
25
+ deny) DF_BRIDGE_OUTCOME=allow; DF_BRIDGE_CODE=decision-mismatch; DF_BRIDGE_REASON=decision-mismatch; DF_BRIDGE_POINT=decision-mismatch ;;
26
+ *) DF_BRIDGE_OUTCOME=allow; DF_BRIDGE_CODE=allow; DF_BRIDGE_REASON=allow; DF_BRIDGE_POINT=""; DF_BRIDGE_VERSION=0 ;;
27
+ esac
28
+ elif [ "$code" -ge 129 ] && [ "$code" -le 255 ] && [ "$code" -ne 255 ] 2>/dev/null; then
29
+ local sig=""
30
+ sig="$(kill -l "$((code - 128))" 2>/dev/null)" || sig=""
31
+ sig="${sig#SIG}"
32
+ case "$sig" in ''|*[!A-Za-z0-9_-]*) return;; esac
33
+ DF_BRIDGE_OUTCOME=error; DF_BRIDGE_CODE=signal-"$sig"; DF_BRIDGE_REASON="$DF_BRIDGE_CODE"; DF_BRIDGE_POINT="eval-error:signal-$sig"
34
+ fi
35
+ }
36
+
37
+ doflow_bridge_effect() {
38
+ local harness="$1" event="$2" policy="$3" outcome="$4" profile="${DOFLOW_FAULT_PROFILE:-compat-1}" block=0
39
+ doflow_bridge_native_event_var "$harness" "$event"; event=$DF_NE
40
+ if [ "$profile" != compat-1 ] && [ "$profile" != strict-1 ]; then
41
+ profile=compat-1; doflow_bridge_failure "$policy" fault-profile-unknown
42
+ fi
43
+ DF_BRIDGE_PROFILE="$profile"
44
+ case "$policy" in pre-implementation-gate|pre-bash-guard|mcp-tool-guard|stop-check) block=1;; esac
45
+ DF_BRIDGE_EFFECT=proceed
46
+ if [ "$outcome" = deny ]; then DF_BRIDGE_EFFECT=block
47
+ elif [ "$profile" = strict-1 ] && [ "$block" -eq 1 ] && [ "$policy" != stop-check ] && { [ "$outcome" = error ] || [ "$outcome" = unsupported ] || [ "$DF_BRIDGE_CODE" = bridge-spawn ] || [ "$DF_BRIDGE_CODE" = envelope-encode ]; }; then DF_BRIDGE_EFFECT=block
48
+ elif [ "$outcome" = error ] && [ "$DF_BRIDGE_CODE" != bridge-spawn ] && [ -n "$DF_BRIDGE_POINT" ] && [ "$profile" = compat-1 ]; then
49
+ case "$harness:$event" in
50
+ claude:PreToolUse|codex:PreToolUse|gemini:BeforeTool|pi:tool_call) DF_BRIDGE_EFFECT=block ;;
51
+ antigravity:PreToolUse|antigravity:Stop) case "$DF_BRIDGE_CODE" in signal-*) DF_BRIDGE_EFFECT=proceed;; *) DF_BRIDGE_EFFECT=block;; esac ;;
52
+ kiro:PreToolUse) [ "$DF_BRIDGE_CODE" = exit-2-unmarked ] && DF_BRIDGE_EFFECT=block ;;
53
+ claude:Stop) [ "$DF_BRIDGE_CODE" = exit-2-unmarked ] && DF_BRIDGE_EFFECT=block ;;
54
+ codex:Stop|codex:stop|kiro:Stop|kiro:stop) [ "$DF_BRIDGE_CODE" = exit-2-unmarked ] && DF_BRIDGE_EFFECT=block || DF_BRIDGE_EFFECT=proceed ;;
55
+ esac
56
+ fi
57
+ }
58
+
59
+ doflow_bridge_drops_tool_streams() {
60
+ case "$1" in
61
+ pre-implementation-gate|pre-bash-guard|mcp-tool-guard) ;;
62
+ *) return 1 ;;
63
+ esac
64
+ case "$2:$3" in
65
+ claude:PreToolUse|codex:PreToolUse|gemini:BeforeTool|kiro:PreToolUse|antigravity:PreToolUse|antigravity:Stop|pi:tool_call) return 0 ;;
66
+ *) return 1 ;;
67
+ esac
68
+ }
69
+
70
+ doflow_bridge_drops_tool_stderr() {
71
+ case "$1:$2:$3" in
72
+ pre-implementation-gate:kiro:PreToolUse|pre-bash-guard:kiro:PreToolUse|mcp-tool-guard:kiro:PreToolUse) return 1 ;;
73
+ esac
74
+ doflow_bridge_drops_tool_streams "$1" "$2" "$3"
75
+ }
76
+
77
+ doflow_bridge() {
78
+ local harness="${1:-}" event="${2:-}" policy="${3:-}" response_event="" emit="native" payload="" encoded="" policy_path="" captured="" err="" code=0 policy_code=0 record="" reason="" decision_reason="" stdout="" status=0 decision="" line="" spawn_failed=0 encode_failed=0
79
+ . "$_DOFLOW_BRIDGE_DIR/respond.sh"
80
+ doflow_bridge_native_event_var "$harness" "$event"; response_event=$DF_NE
81
+ if [ "${4:-}" = --emit ]; then emit="${5:-native}"; fi
82
+ payload="$(cat)"
83
+ if command -v jq >/dev/null 2>&1; then
84
+ encoded="$(printf '%s\n' "$payload" | DOFLOW_TASK_ID="${DOFLOW_TASK_ID:-}" DOFLOW_RUN_ID="${DOFLOW_RUN_ID:-}" DOFLOW_ATTEMPT="${DOFLOW_ATTEMPT:-}" DOFLOW_SESSION_ROLE="${DOFLOW_SESSION_ROLE:-}" jq -c --arg harness "$harness" --arg event "$event" --arg policy "$policy" --arg mode encode -f "$_DOFLOW_BRIDGE_DIR/envelope.jq" 2>/dev/null)" && {
85
+ case "$encoded" in \{*) payload="$encoded" ;; *) encode_failed=1 ;; esac
86
+ } || { encode_failed=1; }
87
+ if [ "$encode_failed" -eq 1 ]; then
88
+ encoded="$(jq -cn --arg harness "$harness" --arg event "$event" --arg policy "$policy" --arg mode minimal -f "$_DOFLOW_BRIDGE_DIR/envelope.jq" 2>/dev/null)" || encoded=""
89
+ case "$encoded" in \{*) payload="$encoded" ;; *) payload="{\"doflow_event\":{\"version\":1,\"harness\":\"$harness\",\"event\":\"$event\",\"operation\":\"tool\"}}" ;; esac
90
+ doflow_bridge_failure "$policy" envelope-encode
91
+ fi
92
+ else
93
+ doflow_bridge_failure "$policy" dependency-missing:jq
94
+ fi
95
+ case "$policy" in
96
+ pre-implementation-gate|pre-bash-guard|mcp-tool-guard|stop-check|session-context|user-prompt-submit|subagent-audit) ;;
97
+ *) spawn_failed=1 ;;
98
+ esac
99
+ policy_path="$_DOFLOW_BRIDGE_DIR/../policies/$policy.sh"
100
+ if [ "$spawn_failed" -eq 1 ] || [ ! -f "$policy_path" ]; then
101
+ code=127; err=""; spawn_failed=1
102
+ else
103
+ if [ "$emit" = decision ] || doflow_bridge_drops_tool_streams "$policy" "$harness" "$response_event" || { [ "$harness" = kiro ] && [ "$response_event" = Stop ]; }; then exec 5>/dev/null; else exec 5>&1; fi
104
+ captured="$( {
105
+ if DOFLOW_AGENT="${DOFLOW_AGENT:-$harness}" DOFLOW_DECISION_FD=3 bash "$policy_path" 2>&1 3>&1 1>&5 5>&- <<<"$payload"; then policy_code=0; else policy_code=$?; fi
106
+ printf '\037%s' "$policy_code"
107
+ } )"
108
+ exec 5>&-
109
+ code="${captured##*$'\037'}"
110
+ captured="${captured%$'\037'*}"
111
+ fi
112
+ while IFS= read -r line || [ -n "$line" ]; do
113
+ case "$line" in
114
+ *$'\036'*)
115
+ local prefix="${line%%$'\036'*}"
116
+ record="${line#*$'\036'}"
117
+ [ -z "$prefix" ] || err="$err$prefix"$'\n'
118
+ ;;
119
+ *) err="$err$line"$'\n' ;;
120
+ esac
121
+ done < <(printf '%s' "$captured")
122
+ reason="${err%$'\n'}"
123
+ doflow_bridge_classify "$code" "$record" "$reason"
124
+ if [ "$encode_failed" -eq 1 ] && [ "$DF_BRIDGE_OUTCOME" != deny ]; then DF_BRIDGE_CODE=envelope-encode; DF_BRIDGE_POINT=envelope-encode; fi
125
+ if [ "$spawn_failed" -eq 1 ]; then DF_BRIDGE_OUTCOME=error; DF_BRIDGE_CODE=bridge-spawn; DF_BRIDGE_REASON=bridge-spawn; DF_BRIDGE_VERSION=0; DF_BRIDGE_POINT=bridge-spawn; fi
126
+ if [ -n "$DF_BRIDGE_POINT" ] && [ "$DF_BRIDGE_POINT" != decision-mismatch ] && [ "$DF_BRIDGE_POINT" != eval-error:signal-TERM ] && [ "$DF_BRIDGE_POINT" != eval-error:signal-INT ] && [ "$DF_BRIDGE_POINT" != eval-error:signal-PIPE ]; then doflow_bridge_failure "$policy" "$DF_BRIDGE_POINT"; fi
127
+ doflow_bridge_effect "$harness" "$event" "$policy" "$DF_BRIDGE_OUTCOME"
128
+ if [ "$DF_BRIDGE_EFFECT" = block ] && [ "$DF_BRIDGE_OUTCOME" != deny ]; then reason="[$policy] DoFlow policy error; details: doflow-run failure list"; fi
129
+ if [ "$DF_BRIDGE_OUTCOME" != deny ] && [ "$DF_BRIDGE_EFFECT" = proceed ]; then
130
+ case "$harness:$response_event:$DF_BRIDGE_OUTCOME" in
131
+ kiro:PreToolUse:error|claude:Stop:error)
132
+ case "$DF_BRIDGE_CODE" in
133
+ exit-*|signal-*)
134
+ reason="[$policy] DoFlow policy error; details: doflow-run failure list"
135
+ if [ "$harness:$response_event" = claude:Stop ]; then reason="$reason"$'\n'; fi
136
+ ;;
137
+ esac
138
+ ;;
139
+ *) reason="$DF_BRIDGE_CODE" ;;
140
+ esac
141
+ fi
142
+ decision_reason="$DF_BRIDGE_CODE"
143
+ if [ "$DF_BRIDGE_OUTCOME" = deny ]; then decision_reason="$reason"; fi
144
+ if [ "$emit" = decision ] || [ -n "${DOFLOW_BRIDGE_TRACE:-}" ]; then
145
+ decision="$(jq -cn --arg harness "$harness" --arg event "$event" --arg policy "$policy" --arg outcome "$DF_BRIDGE_OUTCOME" --arg code "$DF_BRIDGE_CODE" --arg reason "$decision_reason" --arg profile "$DF_BRIDGE_PROFILE" --arg effect "$DF_BRIDGE_EFFECT" --argjson version "$DF_BRIDGE_VERSION" '{v:1,harness:$harness,event:$event,policy:$policy,policyVersion:$version,outcome:$outcome,code:$code,reason:$reason,faultProfile:$profile,effect:$effect}')"
146
+ fi
147
+ if [ -n "${DOFLOW_BRIDGE_TRACE:-}" ]; then printf '%s\n' "$decision" >>"$DOFLOW_BRIDGE_TRACE" 2>/dev/null || true; fi
148
+ if [ "$emit" = decision ]; then
149
+ printf '%s\n' "$decision"
150
+ return 0
151
+ fi
152
+ if [ "$DF_BRIDGE_EFFECT" = block ]; then
153
+ status=2
154
+ else
155
+ status=0
156
+ if [ "$DF_BRIDGE_OUTCOME" = error ]; then
157
+ case "$policy:$harness:$response_event" in
158
+ session-context:*:*|user-prompt-submit:*:*|subagent-audit:*:*|*:kiro:PreToolUse|*:claude:Stop) status="$code" ;;
159
+ esac
160
+ fi
161
+ fi
162
+ if [ "$spawn_failed" -eq 1 ] && [ "$DF_BRIDGE_EFFECT" = proceed ]; then status=0; fi
163
+ if [ "$DF_BRIDGE_EFFECT" = block ]; then
164
+ doflow_render_response "$harness" "$event" deny "$reason" "$status"
165
+ local render_status=$?
166
+ return "$render_status"
167
+ fi
168
+ case "$policy:$DF_BRIDGE_OUTCOME:$harness:$response_event:$DF_BRIDGE_CODE" in
169
+ session-context:error:*:*:*|user-prompt-submit:error:*:*:*|subagent-audit:error:*:*:*|session-context:unsupported:*:*:*|user-prompt-submit:unsupported:*:*:*|subagent-audit:unsupported:*:*:*) ;;
170
+ *:error:codex:Stop:exit-*|*:error:codex:Stop:signal-*|*:error:codex:stop:exit-*|*:error:codex:stop:signal-*|*:error:kiro:PreToolUse:exit-*|*:error:kiro:PreToolUse:signal-*|*:error:kiro:Stop:exit-*|*:error:kiro:Stop:signal-*|*:error:claude:Stop:exit-*|*:error:claude:Stop:signal-*) ;;
171
+ *)
172
+ if ! doflow_bridge_drops_tool_stderr "$policy" "$harness" "$response_event" && [ -n "$err" ]; then printf '%s' "$err" >&2; fi
173
+ ;;
174
+ esac
175
+ doflow_render_response "$harness" "$event" "$DF_BRIDGE_OUTCOME" "$reason" "$status"
176
+ local render_status=$?
177
+ return "$render_status"
178
+ }
179
+
180
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
181
+ . "$_DOFLOW_BRIDGE_DIR/respond.sh"
182
+ doflow_bridge "${1:-}" "${2:-}" "${3:-}" "${4:-}" "${5:-}"
183
+ exit $?
184
+ fi
@@ -0,0 +1,60 @@
1
+ def nonempty($v): if ($v | type) == "string" and $v != "" then $v else null end;
2
+ def put($o; $k; $v): if $v == null then $o else $o + {($k): $v} end;
3
+ def event_name($fallback): if (.hook_event_name | type) == "string" then .hook_event_name else $fallback end;
4
+ def path_values($obj; $keys): first([$keys[] as $k | $obj[$k] | select(type == "string" and length > 0)][]) // null | if . == null then null else [.] end;
5
+ def string_value($obj; $keys): first([$keys[] as $k | $obj[$k] | select(type == "string") ][]) // null;
6
+ def session_identity($s): {session: nonempty($s)};
7
+ def correlation($id;$parent): {toolCallId:nonempty($id),parentToolCallId:nonempty($parent)};
8
+ def event_operation($e):
9
+ if $e == "SessionStart" or $e == "agentSpawn" then "session-start"
10
+ elif $e == "SessionEnd" then "session-end"
11
+ elif $e == "UserPromptSubmit" then "prompt"
12
+ elif $e == "Stop" or $e == "stop" then "stop"
13
+ elif $e == "SubagentStart" then "subagent-start"
14
+ elif $e == "SubagentStop" then "subagent-stop"
15
+ elif $e == "PreCompact" or $e == "PreCompress" then "compact"
16
+ elif $e == "PostToolUse" or $e == "AfterTool" then "post-tool"
17
+ else "tool" end;
18
+ def canonical_tool($name; $harness):
19
+ if ($name | type) != "string" then {operation:"tool"}
20
+ elif (($harness == "claude" or $harness == "codex" or $harness == "kiro") and ($name | test("^mcp__"))) or (($harness == "gemini" or $harness == "antigravity") and ($name | test("^mcp_"))) or ($harness == "pi" and ($name | test("^mcp__"))) then {operation:"mcp",tool:$name}
21
+ elif (($harness == "claude" or $harness == "codex" or $harness == "kiro") and (["Edit","Write","MultiEdit","apply_patch"] | index($name)) != null) or ($harness == "gemini" and (["write_file","replace"] | index($name)) != null) or ($harness == "antigravity" and (["write_to_file","replace_file_content","multi_replace_file_content","edit_file","write_file","create_file","replace_file","replace","Edit","Write","MultiEdit","apply_patch"] | index($name)) != null) or ($harness == "pi" and (["edit","write"] | index($name)) != null) then {operation:"edit",tool:$name}
22
+ elif (($harness == "claude" or $harness == "codex" or $harness == "kiro") and $name == "Bash") or ($harness == "gemini" and $name == "run_shell_command") or ($harness == "antigravity" and (["run_command","run_shell_command","Bash","bash"] | index($name)) != null) or ($harness == "pi" and $name == "bash") then {operation:"command",tool:$name}
23
+ else {operation:"tool",tool:$name} end;
24
+ def decode_claude:
25
+ (canonical_tool(.tool_name // ""; "claude")) as $t |
26
+ if .tool_name == null then {operation: event_operation($event), identity: session_identity(.session_id)}
27
+ else {operation:$t.operation, tool:$t.tool, paths:(if $t.operation == "edit" then path_values(.tool_input; ["file_path"]) else null end), command:(if $t.operation == "command" then string_value(.tool_input; ["command"]) else null end), identity:session_identity(.session_id)} end;
28
+ def decode_codex:
29
+ decode_claude as $d |
30
+ if .tool_name == "apply_patch" then $d + {patch:(string_value(.tool_input; ["command"]))} else $d end;
31
+ def decode_gemini:
32
+ (canonical_tool(.tool_name // ""; "gemini")) as $t |
33
+ if .tool_name == null then {operation:event_operation($event),identity:session_identity(.session_id)}
34
+ else {operation:$t.operation,tool:$t.tool,paths:(if $t.operation == "edit" then path_values(.tool_input; ["file_path","path"]) else null end),command:(if $t.operation == "command" then string_value(.tool_input; ["command"]) else null end),identity:session_identity(.session_id)} end;
35
+ def decode_kiro:
36
+ (if ((.tool_name // "") | type) == "string" and ((.tool_name // "") | startswith("mcp_")) then {operation:"mcp",tool:.tool_name}
37
+ elif ((.tool_name // "") | type) == "string" and ((.tool_name // "") | test("^@[^/]+/.+")) then
38
+ ((.tool_name | capture("^@(?<server>[^/]+)/(?<tool>.+)$")) as $m | {operation:"mcp",tool:("mcp__" + $m.server + "__" + $m.tool)})
39
+ else canonical_tool(.tool_name // ""; "kiro") end) as $t |
40
+ if .tool_name == null then {operation:event_operation($event),identity:session_identity(.session_id)}
41
+ else {operation:$t.operation,tool:$t.tool,paths:(if $t.operation == "edit" then path_values(.tool_input; ["file_path"]) else null end),command:(if $t.operation == "command" then string_value(.tool_input; ["command"]) else null end),identity:session_identity(.session_id)} end;
42
+ def decode_antigravity:
43
+ if .toolCall == null then {operation:event_operation($event),identity:session_identity(.session_id)}
44
+ else (canonical_tool(.toolCall.name // ""; "antigravity")) as $t |
45
+ {operation:$t.operation,tool:$t.tool,paths:(if $t.operation == "edit" then path_values(.toolCall.args; ["AbsolutePath","TargetFile","Path","file_path","path"]) else null end),command:(if $t.operation == "command" then string_value(.toolCall.args; ["CommandLine","command"]) else null end),identity:session_identity(.session_id)} end;
46
+ def decode_pi:
47
+ (canonical_tool(.toolName // ""; "pi")) as $t |
48
+ {operation:$t.operation,tool:$t.tool,paths:(if $t.operation == "edit" then path_values(.input; ["path"]) else null end),command:(if $t.operation == "command" then string_value(.input; ["command"]) else null end),identity:session_identity(.sessionId),correlation:(correlation(.toolCallId;.parentToolCallId))};
49
+ if ($ARGS.named.mode // "encode") == "minimal" then {doflow_event:{version:(1),harness:$harness,event:(event_name($event)),operation:event_operation($event)}} elif type != "object" then . else
50
+ . as $native |
51
+ ($ENV.DOFLOW_TASK_ID | nonempty(.)) as $task |
52
+ ($ENV.DOFLOW_RUN_ID | nonempty(.)) as $run |
53
+ ($ENV.DOFLOW_ATTEMPT | try tonumber catch null) as $attempt |
54
+ (if $ENV.DOFLOW_SESSION_ROLE == "main" or $ENV.DOFLOW_SESSION_ROLE == "worker" then $ENV.DOFLOW_SESSION_ROLE else null end) as $role |
55
+ (if $harness == "claude" then decode_claude elif $harness == "codex" then decode_codex elif $harness == "gemini" then decode_gemini elif $harness == "kiro" then decode_kiro elif $harness == "antigravity" then decode_antigravity elif $harness == "pi" then decode_pi else {operation:"tool"} end) as $d |
56
+ ({version:1,harness:$harness,event:(event_name($event)),operation:$d.operation,tool:$d.tool,paths:$d.paths,command:$d.command,patch:$d.patch,taskId:$task,
57
+ identity:($d.identity + {run:$run,attempt:$attempt,role:$role} | with_entries(select(.value != null)) | if length > 0 then . else null end),
58
+ correlation:(($d.correlation // {}) | with_entries(select(.value != null)) | if length > 0 then . else null end) } | with_entries(select(.value != null))) as $envelope |
59
+ $native + {doflow_event:$envelope}
60
+ end
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # Native hook response renderers for the shared lifecycle bridge.
3
+ case ${BASH_SOURCE[0]} in */*) _DOFLOW_RESPOND_DIR=${BASH_SOURCE[0]%/*} ;; *) _DOFLOW_RESPOND_DIR=. ;; esac
4
+ . "$_DOFLOW_RESPOND_DIR/../policies/deny-json.sh"
5
+
6
+ doflow_bridge_native_event_var() {
7
+ DF_NE=$2
8
+ case "$1:$2" in
9
+ kiro:preToolUse) DF_NE=PreToolUse ;;
10
+ kiro:agentSpawn) DF_NE=SessionStart ;;
11
+ kiro:stop) DF_NE=Stop ;;
12
+ esac
13
+ }
14
+
15
+ doflow_bridge_native_event() {
16
+ local harness="$1" event="$2"
17
+ case "$harness:$event" in
18
+ kiro:preToolUse) event=PreToolUse ;;
19
+ kiro:agentSpawn) event=SessionStart ;;
20
+ kiro:stop) event=Stop ;;
21
+ esac
22
+ printf '%s\n' "$event"
23
+ }
24
+
25
+ doflow_render_response() {
26
+ local harness="$1" event="$2" outcome="$3" reason="$4" status="$5" engine="${DOFLOW_KIRO_ENGINE:-2x}"
27
+ doflow_bridge_native_event_var "$harness" "$event"; event=$DF_NE
28
+ if [ "$outcome" = deny ]; then
29
+ case "$harness:$event" in
30
+ claude:PreToolUse|codex:PreToolUse) emit_pretooluse_deny_nested "$reason" || return $?; return 0 ;;
31
+ gemini:BeforeTool) emit_pretooluse_deny_flat "$reason"; return 0 ;;
32
+ kiro:PreToolUse) printf '%s' "$reason" >&2; return 2 ;;
33
+ antigravity:PreToolUse)
34
+ if command -v jq >/dev/null 2>&1; then jq -cn --arg reason "$reason" '{decision:"deny",reason:$reason}'
35
+ else printf '{"decision":"deny","reason":%s}\n' "$( _doflow_json_reason "$reason" )"; fi
36
+ return $? ;;
37
+ antigravity:Stop)
38
+ if command -v jq >/dev/null 2>&1; then jq -cn --arg reason "$reason" '{decision:"continue",reason:$reason}'
39
+ else printf '{"decision":"continue","reason":%s}\n' "$( _doflow_json_reason "$reason" )"; fi
40
+ return $? ;;
41
+ pi:tool_call)
42
+ if command -v jq >/dev/null 2>&1; then jq -cn --arg reason "$reason" '{block:true,reason:$reason}'
43
+ else printf '{"block":true,"reason":%s}\n' "$( _doflow_json_reason "$reason" )"; fi
44
+ return $? ;;
45
+ kiro:Stop|kiro:stop)
46
+ if [ "$engine" = v3 ]; then
47
+ if command -v jq >/dev/null 2>&1; then jq -cn --arg reason "$reason" '{decision:"block",reason:$reason}'
48
+ else printf '{"decision":"block","reason":%s}\n' "$( _doflow_json_reason "$reason" )"; fi
49
+ return $?
50
+ fi
51
+ printf '%s' "$reason" >&2; return 2 ;;
52
+ codex:Stop|codex:stop) printf '%s\n' '{}' >&1; printf '%s\n' "$reason" >&2; return 2 ;;
53
+ *) printf '%s\n' "$reason" >&2; return 2 ;;
54
+ esac
55
+ fi
56
+ case "$harness:$event" in
57
+ codex:Stop|codex:stop) printf '%s\n' '{}' ;;
58
+ antigravity:PreToolUse) printf '%s\n' '{"decision":"allow"}' ;;
59
+ kiro:PreToolUse|claude:Stop) if [ "$status" -ne 0 ]; then printf '%s' "$reason" >&2; fi ;;
60
+ esac
61
+ return "$status"
62
+ }