@renzrollon/interlock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +58 -0
  3. package/CHANGELOG.md +380 -0
  4. package/LICENSE +21 -0
  5. package/README.md +358 -0
  6. package/agents/ping.md +8 -0
  7. package/agents/worker.md +8 -0
  8. package/bin/interlock +2655 -0
  9. package/bin/interlock-graph +6 -0
  10. package/bin/interlock-run +508 -0
  11. package/bin/interlock-ship-acp +39 -0
  12. package/hooks/_shared.mjs +162 -0
  13. package/hooks/guard-commit.mjs +77 -0
  14. package/hooks/guard-tasks.mjs +107 -0
  15. package/hooks/guard-tests.mjs +63 -0
  16. package/hooks/preflight.mjs +107 -0
  17. package/lib/artifacts.mjs +316 -0
  18. package/lib/autonomy.mjs +149 -0
  19. package/lib/conformance.mjs +132 -0
  20. package/lib/doctor.mjs +1176 -0
  21. package/lib/drift.mjs +409 -0
  22. package/lib/eval-history.mjs +304 -0
  23. package/lib/evals-calibrate.mjs +317 -0
  24. package/lib/evals-capture.mjs +437 -0
  25. package/lib/evals-promote.mjs +257 -0
  26. package/lib/evals-triage.mjs +216 -0
  27. package/lib/findings.mjs +340 -0
  28. package/lib/graph/cli.mjs +266 -0
  29. package/lib/graph/extract/config-json.mjs +74 -0
  30. package/lib/graph/extract/create-extractor.mjs +35 -0
  31. package/lib/graph/extract/memory.mjs +99 -0
  32. package/lib/graph/extract/openspec.mjs +80 -0
  33. package/lib/graph/extract/python.mjs +137 -0
  34. package/lib/graph/extract/regex-fallback.mjs +187 -0
  35. package/lib/graph/extract/shell.mjs +71 -0
  36. package/lib/graph/extract/typescript.mjs +217 -0
  37. package/lib/graph/extract/walk.mjs +272 -0
  38. package/lib/graph/format.mjs +51 -0
  39. package/lib/graph/graph/build.mjs +374 -0
  40. package/lib/graph/graph/query.mjs +249 -0
  41. package/lib/graph/graph/report.mjs +122 -0
  42. package/lib/graph/graph/update.mjs +47 -0
  43. package/lib/graph/paths.mjs +66 -0
  44. package/lib/graph/retrieve/context.mjs +86 -0
  45. package/lib/graph/retrieve/docs-index.mjs +116 -0
  46. package/lib/graph/retrieve/markdown.mjs +246 -0
  47. package/lib/host/acp.mjs +551 -0
  48. package/lib/host/claude-cli.mjs +285 -0
  49. package/lib/host/codex.mjs +231 -0
  50. package/lib/host/model-map.mjs +129 -0
  51. package/lib/host/qwen.mjs +176 -0
  52. package/lib/host/registry.mjs +213 -0
  53. package/lib/host.mjs +393 -0
  54. package/lib/ledger.mjs +444 -0
  55. package/lib/limits.mjs +525 -0
  56. package/lib/locators.mjs +99 -0
  57. package/lib/merge-lanes.mjs +148 -0
  58. package/lib/metrics.mjs +99 -0
  59. package/lib/notify.mjs +229 -0
  60. package/lib/outcomes.mjs +560 -0
  61. package/lib/plan-fingerprint.mjs +570 -0
  62. package/lib/project-slug.mjs +18 -0
  63. package/lib/prompts/commit.mjs +26 -0
  64. package/lib/prompts/dimensions.mjs +70 -0
  65. package/lib/prompts/handoff.mjs +86 -0
  66. package/lib/prompts/implementer.mjs +150 -0
  67. package/lib/prompts/index.mjs +250 -0
  68. package/lib/prompts/planner.mjs +70 -0
  69. package/lib/prompts/remediate.mjs +137 -0
  70. package/lib/prompts/replan.mjs +23 -0
  71. package/lib/prompts/review.mjs +107 -0
  72. package/lib/prompts/schemas.mjs +206 -0
  73. package/lib/prompts/stage.mjs +48 -0
  74. package/lib/prompts/verify.mjs +88 -0
  75. package/lib/ready.mjs +812 -0
  76. package/lib/receipt.mjs +434 -0
  77. package/lib/remediate.mjs +275 -0
  78. package/lib/report-html.mjs +623 -0
  79. package/lib/report.mjs +1124 -0
  80. package/lib/review-core.mjs +713 -0
  81. package/lib/risk.mjs +370 -0
  82. package/lib/run-log.mjs +1004 -0
  83. package/lib/run-paths.mjs +239 -0
  84. package/lib/run.mjs +2921 -0
  85. package/lib/ship-stage.mjs +223 -0
  86. package/lib/spill.mjs +150 -0
  87. package/lib/surface.mjs +195 -0
  88. package/lib/verify.mjs +1074 -0
  89. package/lib/waves.mjs +2661 -0
  90. package/package.json +40 -0
  91. package/shared/CONTEXT-HYGIENE.md +84 -0
  92. package/shared/DECISION-LEDGER.md +141 -0
  93. package/shared/EXPLORE-BRIEF.md +96 -0
  94. package/shared/INVARIANT-SWEEP.md +57 -0
  95. package/shared/SESSION-SHAPES.md +261 -0
  96. package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
  97. package/shared/TEST-PROFILE.md +204 -0
  98. package/shared/TOOL-ECONOMY.md +155 -0
  99. package/skills/bootstrap/SKILL.md +226 -0
  100. package/skills/commit/SKILL.md +99 -0
  101. package/skills/dispatch/SKILL.md +78 -0
  102. package/skills/docs-digest/SKILL.md +109 -0
  103. package/skills/evals/SKILL.md +160 -0
  104. package/skills/explain-code/SKILL.md +42 -0
  105. package/skills/explain-code/commit-teachin.md +136 -0
  106. package/skills/explain-code/module-walkthrough.md +358 -0
  107. package/skills/explore/SKILL.md +159 -0
  108. package/skills/fix-tests/SKILL.md +120 -0
  109. package/skills/graph/SKILL.md +136 -0
  110. package/skills/manual-test-plan/SKILL.md +121 -0
  111. package/skills/mr/SKILL.md +170 -0
  112. package/skills/report/SKILL.md +121 -0
  113. package/skills/review-artifacts/SKILL.md +127 -0
  114. package/skills/review-code/REVIEW.template.md +49 -0
  115. package/skills/review-code/SKILL.md +156 -0
  116. package/skills/review-code/dimensions/architecture.md +10 -0
  117. package/skills/review-code/dimensions/devops.md +12 -0
  118. package/skills/review-code/dimensions/language.md +12 -0
  119. package/skills/review-code/dimensions/qa.md +12 -0
  120. package/skills/review-code/dimensions/security.md +12 -0
  121. package/skills/review-code/dimensions/technical-lead.md +19 -0
  122. package/skills/ship/SKILL.md +67 -0
  123. package/skills/spec/SKILL.md +264 -0
  124. package/skills/spec/continuity.md +66 -0
  125. package/workflows/ship.js +565 -0
@@ -0,0 +1,176 @@
1
+ // The `qwen` host adapter (design D5).
2
+ //
3
+ // Qwen Code's non-interactive mode. One `qwen` process per spawn, prompt on
4
+ // stdin, a JSON envelope on stdout.
5
+ //
6
+ // The contract, verified against the installed CLI (qwen-code 0.22.2) on
7
+ // 2026-09-05:
8
+ //
9
+ // -p, --prompt <text> "Prompt. Appended to input on stdin (if any)."
10
+ // The briefing travels on stdin (design D2), so `-`
11
+ // is the placeholder the flag requires.
12
+ // -o, --output-format json text | json | stream-json
13
+ // --json-schema <json> accepted (absent from `--help`, accepted by the
14
+ // parser — verified by a live argv probe)
15
+ // --yolo accepted, same as above; unattended tool approval
16
+ // -m, --model <MODEL> the model flag, spelled `-m`, verified in `--help`
17
+ //
18
+ // `--yolo` is the reason this host declares `hooks: false` twice over: Qwen has
19
+ // no equivalent of this repository's PreToolUse guards, AND its own approval
20
+ // prompt is disabled because there is nobody to answer it. The runner banners
21
+ // `HOOKS NOT IN FORCE (qwen)`, and the CLI's unit-suite shrink check stays the
22
+ // deterministic backstop.
23
+ //
24
+ // Providers are configured in Qwen's own `settings.json`, so an unmapped slug is
25
+ // not an error — it is a spawn that ran on the operator's default, which the
26
+ // runner names in `MODEL ROUTING UNAVAILABLE (qwen)` rather than passing off as
27
+ // the tier the planner asked for.
28
+
29
+ import { assertWorkflowHost, execCapture, mapPipeline, parseAgentJson, runCli } from '../host.mjs'
30
+ import { parseModelMap, resolveModel } from './model-map.mjs'
31
+
32
+ /** Override the binary, for a wrapper or a fixture. */
33
+ export const QWEN_COMMAND_ENV = 'INTERLOCK_QWEN_COMMAND'
34
+
35
+ /** Optional per-spawn wall clock, in ms. 0 disables it. */
36
+ export const QWEN_TIMEOUT_ENV = 'INTERLOCK_QWEN_TIMEOUT_MS'
37
+
38
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
39
+
40
+ /**
41
+ * The exact argv one spawn sends.
42
+ *
43
+ * @param {object} req
44
+ * @param {{model: string|null}} [opts]
45
+ * @returns {string[]}
46
+ */
47
+ export function qwenArgs(req, { model } = {}) {
48
+ const request = req && typeof req === 'object' ? req : {}
49
+ const args = ['-p', '-', '--output-format', 'json']
50
+ if (request.schema) args.push('--json-schema', JSON.stringify(request.schema))
51
+ args.push('--yolo')
52
+ if (typeof model === 'string' && model) args.push('-m', model)
53
+ return args
54
+ }
55
+
56
+ /**
57
+ * The validated payload out of Qwen's JSON envelope.
58
+ *
59
+ * The envelope wraps the model's answer in a `response` field, which with
60
+ * `--json-schema` is the validated object. Older shapes put it at the top level.
61
+ * Both are read, and `parseAgentJson` recovers a payload that arrived as a
62
+ * string — the CLI validated it, so recovering it from its own quoting is not
63
+ * the same gamble the ACP adapter takes on free prose.
64
+ *
65
+ * @param {string} stdout
66
+ * @returns {{result: object|null, usage: object|null}}
67
+ */
68
+ export function readQwenEnvelope(stdout) {
69
+ let envelope
70
+ try {
71
+ envelope = JSON.parse(typeof stdout === 'string' ? stdout : '')
72
+ } catch {
73
+ return { result: null, usage: null }
74
+ }
75
+ if (!envelope || typeof envelope !== 'object') return { result: null, usage: null }
76
+
77
+ const candidate = envelope.response !== undefined ? envelope.response : envelope
78
+ let result = null
79
+ if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) result = candidate
80
+ else if (typeof candidate === 'string') result = parseAgentJson(candidate)
81
+
82
+ const stats = envelope.stats && typeof envelope.stats === 'object' ? envelope.stats : null
83
+ const tokens = stats && stats.tokens && typeof stats.tokens === 'object' ? stats.tokens : null
84
+ const usage =
85
+ tokens && (Number.isFinite(tokens.output) || Number.isFinite(tokens.input))
86
+ ? {
87
+ inputTokens: Number.isFinite(tokens.input) ? tokens.input : undefined,
88
+ outputTokens: Number.isFinite(tokens.output) ? tokens.output : undefined
89
+ }
90
+ : null
91
+
92
+ return { result, usage }
93
+ }
94
+
95
+ /**
96
+ * Build a {@link import('../host.mjs').WorkflowHost} backed by `qwen -p`.
97
+ *
98
+ * @param {object} [opts]
99
+ * @param {string} [opts.command] defaults to `$INTERLOCK_QWEN_COMMAND` or `qwen`
100
+ * @param {string} [opts.cwd]
101
+ * @param {object} [opts.env]
102
+ * @param {number} [opts.timeoutMs]
103
+ * @param {(event: object) => void} [opts.onEvent]
104
+ */
105
+ export function createQwenHost({ command, cwd = process.cwd(), env = process.env, timeoutMs, onEvent } = {}) {
106
+ const source = env && typeof env === 'object' ? env : {}
107
+ const raw = (typeof command === 'string' && command.trim()) || source[QWEN_COMMAND_ENV] || 'qwen'
108
+ const argv = String(raw).trim().split(/\s+/)
109
+ const bin = argv[0]
110
+ const fixedArgs = argv.slice(1)
111
+ const budget = Number.isInteger(timeoutMs)
112
+ ? timeoutMs
113
+ : Number.parseInt(source[QWEN_TIMEOUT_ENV] || '', 10) || DEFAULT_TIMEOUT_MS
114
+ const modelMap = parseModelMap(source)
115
+
116
+ const host = {
117
+ command: [bin, ...fixedArgs].join(' '),
118
+
119
+ async spawn(req) {
120
+ const request = req && typeof req === 'object' ? req : {}
121
+ const label = typeof request.label === 'string' ? request.label : 'agent'
122
+ const started = Date.now()
123
+ const routed = resolveModel('qwen', request.model, modelMap, { modelSelect: 'map-only' })
124
+ if (typeof request.model === 'string' && request.model && onEvent) {
125
+ onEvent({
126
+ type: 'model-routing',
127
+ label,
128
+ requested: request.model,
129
+ applied: routed.applied,
130
+ value: routed.value,
131
+ via: routed.applied ? 'flag' : null,
132
+ reason: routed.reason
133
+ })
134
+ }
135
+
136
+ const { code, stdout, stderr, timedOut } = await execCapture(
137
+ bin,
138
+ [...fixedArgs, ...qwenArgs(request, { model: routed.value })],
139
+ {
140
+ cwd: typeof request.cwd === 'string' && request.cwd ? request.cwd : cwd,
141
+ env: source,
142
+ input: request.prompt,
143
+ timeoutMs: budget
144
+ }
145
+ )
146
+
147
+ if (code !== 0) {
148
+ if (onEvent) {
149
+ onEvent({
150
+ type: 'spawn-failed',
151
+ label,
152
+ code,
153
+ timedOut,
154
+ error: stderr.trim().slice(-500) || `qwen exited ${code}`
155
+ })
156
+ }
157
+ return null
158
+ }
159
+
160
+ const { result, usage } = readQwenEnvelope(stdout)
161
+ if (onEvent) {
162
+ onEvent({ type: 'spawn-done', label, ms: Date.now() - started, parsed: Boolean(result), usage })
163
+ }
164
+ if (!result) return null
165
+ return usage ? { ...result, usage } : result
166
+ },
167
+
168
+ mapPipeline,
169
+
170
+ runCli(argv2, opts = {}) {
171
+ return runCli(argv2, { cwd, env: source, ...opts })
172
+ }
173
+ }
174
+
175
+ return assertWorkflowHost(host, 'qwen host')
176
+ }
@@ -0,0 +1,213 @@
1
+ // The host registry (design D1).
2
+ //
3
+ // `bin/interlock-run` is an interpreter of the program `interlock run` emits. It
4
+ // spawns what a step names and calls the argv a step names. The only thing that
5
+ // differs between running that program over Claude Code, over an ACP agent, over
6
+ // Codex or over Qwen is the transport — so a host is one file under this
7
+ // directory implementing the three-function port in `../host.mjs`, plus a
8
+ // declaration of what it cannot do.
9
+ //
10
+ // The declaration is the load-bearing half. Every adapter states the same seven
11
+ // facts, and the run program reads them rather than branching on an id:
12
+ //
13
+ // schemaEnforced does the CLI itself validate the result against the schema
14
+ // modelSelect 'flag' | 'negotiated' | 'map-only' — how a slug becomes a model
15
+ // worktree 'driver' | 'runtime' — who creates a lane's worktree
16
+ // hooks do this repository's PreToolUse guards fire on this host
17
+ // usage does the CLI report token usage
18
+ // cacheAccounting does it decompose that usage into cache reads and writes
19
+ // billing which account the run spends against
20
+ //
21
+ // `cacheAccounting` is separate from `usage` and not implied by it: the Workflow
22
+ // runtime demonstrates the gap by exposing a single cumulative spend scalar with
23
+ // no decomposition at all, and a run cannot banner a capability nobody declared.
24
+ //
25
+ // A capability that were merely implied by the adapter's code would be a
26
+ // capability nothing could banner, and every one of these describes a way a run
27
+ // is WEAKER than the default host. `test/spine/host-registry.test.mjs` asserts
28
+ // each adapter declares every key, so a fifth adapter cannot arrive with a
29
+ // silently absent one.
30
+ //
31
+ // The Workflow host is deliberately absent: it is reached through
32
+ // `/interlock:ship`, it owns its own worktrees (`worktree: 'runtime'`), and the
33
+ // runner refuses its id (design D10).
34
+
35
+ import { createAcpHost, isClaudeCodeBinary, parseAcpCommand, ACP_COMMAND_ENV } from './acp.mjs'
36
+ import { createClaudeHost } from './claude-cli.mjs'
37
+ import { createCodexHost } from './codex.mjs'
38
+ import { createQwenHost } from './qwen.mjs'
39
+
40
+ /** Every fact an adapter must declare. Order is the order a banner reads them. */
41
+ export const CAPABILITY_KEYS = Object.freeze([
42
+ 'schemaEnforced',
43
+ 'modelSelect',
44
+ 'worktree',
45
+ 'hooks',
46
+ 'usage',
47
+ 'cacheAccounting',
48
+ 'billing'
49
+ ])
50
+
51
+ /** The allowed value of each declared capability. */
52
+ export const CAPABILITY_VALUES = Object.freeze({
53
+ schemaEnforced: Object.freeze([true, false]),
54
+ modelSelect: Object.freeze(['flag', 'negotiated', 'map-only']),
55
+ worktree: Object.freeze(['driver', 'runtime']),
56
+ hooks: Object.freeze([true, false]),
57
+ usage: Object.freeze([true, false]),
58
+ cacheAccounting: Object.freeze([true, false]),
59
+ billing: Object.freeze(['claude-subscription-programmatic', 'chatgpt-plan-or-api', 'local'])
60
+ })
61
+
62
+ /**
63
+ * The Workflow host's id. Not an adapter — the runner refuses it, and `run
64
+ * start` treats a run without declared capabilities as this one (design D10).
65
+ * What that assumption CONTAINS is stated once, in `lib/run.mjs`, because the
66
+ * run program is the only party that reads it.
67
+ */
68
+ export const WORKFLOW_HOST_ID = 'workflow'
69
+
70
+ export const HOSTS = Object.freeze({
71
+ claude: {
72
+ id: 'claude',
73
+ create: opts => createClaudeHost(opts),
74
+ capabilities: Object.freeze({
75
+ schemaEnforced: true,
76
+ modelSelect: 'flag',
77
+ worktree: 'driver',
78
+ hooks: true,
79
+ usage: true,
80
+ // The vendor envelope carries `cache_read_input_tokens` and a
81
+ // `cache_creation` block split by lifetime tier; `readClaudeEnvelope`
82
+ // keeps both.
83
+ cacheAccounting: true,
84
+ billing: 'claude-subscription-programmatic'
85
+ })
86
+ },
87
+ acp: {
88
+ id: 'acp',
89
+ create: opts => createAcpHost(opts),
90
+ // Declared for the general case — an arbitrary agent behind
91
+ // `INTERLOCK_ACP_COMMAND`. The adapter refines `hooks` and `billing` at
92
+ // creation when that command turns out to be the Claude binary, because a
93
+ // run that IS spending a Claude subscription must say so (design D8).
94
+ capabilities: Object.freeze({
95
+ schemaEnforced: false,
96
+ modelSelect: 'negotiated',
97
+ worktree: 'driver',
98
+ hooks: false,
99
+ usage: false,
100
+ // No usage envelope to decompose. Refined at creation only where `usage`
101
+ // is, and it is not — an arbitrary ACP agent reports neither.
102
+ cacheAccounting: false,
103
+ billing: 'local'
104
+ })
105
+ },
106
+ codex: {
107
+ id: 'codex',
108
+ create: opts => createCodexHost(opts),
109
+ capabilities: Object.freeze({
110
+ schemaEnforced: true,
111
+ modelSelect: 'map-only',
112
+ worktree: 'driver',
113
+ hooks: false,
114
+ usage: true,
115
+ // Token usage without a cache decomposition — exactly the gap this
116
+ // capability exists to make declarable rather than inferrable.
117
+ cacheAccounting: false,
118
+ billing: 'chatgpt-plan-or-api'
119
+ })
120
+ },
121
+ qwen: {
122
+ id: 'qwen',
123
+ create: opts => createQwenHost(opts),
124
+ capabilities: Object.freeze({
125
+ schemaEnforced: true,
126
+ modelSelect: 'map-only',
127
+ worktree: 'driver',
128
+ hooks: false,
129
+ usage: false,
130
+ cacheAccounting: false,
131
+ billing: 'local'
132
+ })
133
+ }
134
+ })
135
+
136
+ /** Every adapter id the runner accepts, for a message that names them. */
137
+ export const HOST_IDS = Object.freeze(Object.keys(HOSTS))
138
+
139
+ /**
140
+ * Create the adapter an id names.
141
+ *
142
+ * Throws on an unknown id and on the Workflow host, both before any step runs:
143
+ * a run that started and then discovered it had no host would already have
144
+ * written a manifest and a trajectory for a run that never happened.
145
+ *
146
+ * @param {string} id
147
+ * @param {object} [opts] passed through to the adapter's factory
148
+ * @returns {import('../host.mjs').WorkflowHost & {capabilities: object}}
149
+ */
150
+ export function createHost(id, opts = {}) {
151
+ const wanted = typeof id === 'string' ? id.trim() : ''
152
+ if (wanted === WORKFLOW_HOST_ID) {
153
+ throw new Error(
154
+ 'the Workflow runtime is reached through /interlock:ship, not the runner — ' +
155
+ `known runner hosts: ${HOST_IDS.join(', ')}`
156
+ )
157
+ }
158
+ const entry = HOSTS[wanted]
159
+ if (!entry) {
160
+ throw new Error(`unknown host "${wanted || '(none)'}" — known hosts: ${HOST_IDS.join(', ')}`)
161
+ }
162
+ const host = entry.create(opts)
163
+ return Object.assign(host, {
164
+ id: entry.id,
165
+ capabilities: effectiveCapabilities(entry, host)
166
+ })
167
+ }
168
+
169
+ /**
170
+ * The declared capabilities, refined by anything the created host observed
171
+ * about itself.
172
+ *
173
+ * Only ACP refines today, and only downward-to-honest: a `claude` binary behind
174
+ * `INTERLOCK_ACP_COMMAND` means the run does have this repository's hooks and is
175
+ * spending a Claude subscription, and declaring otherwise would suppress the two
176
+ * banners that matter most on that path.
177
+ *
178
+ * @param {{capabilities: object}} entry
179
+ * @param {object} host
180
+ * @returns {object}
181
+ */
182
+ function effectiveCapabilities(entry, host) {
183
+ const declared = { ...entry.capabilities }
184
+ const observed = host && typeof host.capabilities === 'object' && host.capabilities ? host.capabilities : {}
185
+ for (const key of CAPABILITY_KEYS) {
186
+ if (observed[key] !== undefined) declared[key] = observed[key]
187
+ }
188
+ return declared
189
+ }
190
+
191
+ /**
192
+ * Whether a host id, as configured, drives the Claude binary — directly or
193
+ * through ACP. The `SUBSCRIPTION PATH` banner's condition (design D8).
194
+ *
195
+ * @param {string} id
196
+ * @param {Record<string, string|undefined>} [env]
197
+ * @param {string} [acpCommand] an explicit --acp-command, which beats the env
198
+ * @returns {boolean}
199
+ */
200
+ export function drivesClaudeBinary(id, env = {}, acpCommand) {
201
+ if (id === 'claude') return true
202
+ if (id !== 'acp') return false
203
+ const raw = typeof acpCommand === 'string' && acpCommand.trim()
204
+ ? acpCommand
205
+ : (env && env[ACP_COMMAND_ENV]) || ''
206
+ try {
207
+ return isClaudeCodeBinary(parseAcpCommand(raw).command)
208
+ } catch {
209
+ // An unusable command is not a Claude run; the adapter's own creation error
210
+ // is what the operator will see.
211
+ return false
212
+ }
213
+ }