opencode-turnstile 0.2.0 → 0.2.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.
- package/README.md +263 -99
- package/agents/debugger.md +3 -2
- package/agents/executor.md +7 -2
- package/bin/cli.mjs +34 -22
- package/package.json +5 -6
- package/scripts/branch-coverage.mjs +206 -0
- package/scripts/check-markers.mjs +23 -1
- package/scripts/install.mjs +113 -13
- package/scripts/status.mjs +333 -0
- package/skills/turnstile-setup/SKILL.md +22 -38
- package/src/config.ts +295 -6
- package/src/evidence.ts +162 -0
- package/src/gates.ts +338 -93
- package/src/journal.ts +10 -3
- package/src/markers.ts +117 -15
- package/src/policy.ts +33 -0
- package/src/setup.ts +1265 -96
- package/src/state.ts +316 -21
- package/src/tool-policy.ts +683 -0
- package/src/types.ts +300 -1
- package/turnstile.ts +150 -14
- package/scripts/install-agents.mjs +0 -504
- package/templates/global-AGENTS.md +0 -55
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Read-only report of turnstile gate/plan state over the JSONL journal.
|
|
3
|
+
//
|
|
4
|
+
// This is the `turnstile status` surface (issue #34): it reports, per parent
|
|
5
|
+
// session, what the journal says a task currently is WITHOUT touching the host
|
|
6
|
+
// or writing anything. It is deliberately a plain ESM script outside the 100%
|
|
7
|
+
// coverage gate (bin/ and scripts/ never are), and it is exercised by
|
|
8
|
+
// tests/status.test.ts via spawn rather than imported.
|
|
9
|
+
//
|
|
10
|
+
// Journal resolution precedence (mirrors config.ts's env → file → default):
|
|
11
|
+
// TURNSTILE_METRICS_DIR → TURNSTILE_CONFIG's metricsDir → ~ default
|
|
12
|
+
// The journal file is `turnstile.jsonl`; its rotated generation
|
|
13
|
+
// `${journal}.1` is read first so the 2000-line replay tail holds across both.
|
|
14
|
+
//
|
|
15
|
+
// opencode-turnstile status Human-readable table.
|
|
16
|
+
// opencode-turnstile status --json Machine-readable JSON.
|
|
17
|
+
// opencode-turnstile status --help This usage. Unknown flags exit 2.
|
|
18
|
+
|
|
19
|
+
import * as fs from "node:fs"
|
|
20
|
+
import * as os from "node:os"
|
|
21
|
+
import * as path from "node:path"
|
|
22
|
+
|
|
23
|
+
const USAGE = `Usage: opencode-turnstile status [--json]
|
|
24
|
+
|
|
25
|
+
Reports turnstile gate and plan state per parent session, read from the
|
|
26
|
+
JSONL journal. Read-only: nothing is written. The journal reflects the last
|
|
27
|
+
prompt-cycle; in-memory state can be fresher.
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--json Print machine-readable JSON instead of the table
|
|
31
|
+
--help Show this help`
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the default metrics directory (copy of the journaler default so the
|
|
35
|
+
* standalone script needs no TS source). Honors an absolute XDG_DATA_HOME or
|
|
36
|
+
* HOME; anything relative falls back to the OS-provided value.
|
|
37
|
+
* @param {Record<string, string | undefined>} env Environment to consult.
|
|
38
|
+
* @returns {string} The absolute default metrics directory.
|
|
39
|
+
*/
|
|
40
|
+
function defaultMetricsDir(env) {
|
|
41
|
+
const abs = (v) => (typeof v === "string" && path.isAbsolute(v) ? v : undefined)
|
|
42
|
+
const home = abs(env.HOME) ?? os.homedir()
|
|
43
|
+
const base = abs(env.XDG_DATA_HOME) ?? path.join(home, ".local/share")
|
|
44
|
+
return path.join(base, "opencode", "metrics")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the metrics directory that holds the journal, honoring the same
|
|
49
|
+
* precedence the config layer uses: the TURNSTILE_METRICS_DIR env beats a
|
|
50
|
+
* configured metricsDir in the TURNSTILE_CONFIG file, which beats the default.
|
|
51
|
+
* @param {NodeJS.ProcessEnv} env The process environment.
|
|
52
|
+
* @returns {string} The resolved metrics directory.
|
|
53
|
+
*/
|
|
54
|
+
function resolveMetricsDir(env) {
|
|
55
|
+
if (typeof env.TURNSTILE_METRICS_DIR === "string" && env.TURNSTILE_METRICS_DIR) {
|
|
56
|
+
return env.TURNSTILE_METRICS_DIR
|
|
57
|
+
}
|
|
58
|
+
const configPath =
|
|
59
|
+
env.TURNSTILE_CONFIG ?? path.join(os.homedir(), ".config", "opencode", "turnstile.json")
|
|
60
|
+
try {
|
|
61
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"))
|
|
62
|
+
if (cfg && typeof cfg.metricsDir === "string" && cfg.metricsDir) return cfg.metricsDir
|
|
63
|
+
} catch {
|
|
64
|
+
// missing/unreadable/malformed config: fall through to the default.
|
|
65
|
+
}
|
|
66
|
+
return defaultMetricsDir(env)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reads the journal file and its `.1` generation as a single oldest-first line
|
|
71
|
+
* stream, applying the same 2000-line tail bound the runtime replay uses so the
|
|
72
|
+
* status flags stay replay-parity. Silently tolerates a missing generation.
|
|
73
|
+
* @param {string} journalPath Absolute path of `turnstile.jsonl`.
|
|
74
|
+
* @returns {{ files: string[], lines: string[] }} The files read and their lines.
|
|
75
|
+
*/
|
|
76
|
+
function readJournal(journalPath) {
|
|
77
|
+
const generations = [`${journalPath}.1`, journalPath].filter((p) => fs.existsSync(p))
|
|
78
|
+
if (generations.length === 0) return { files: [], lines: [] }
|
|
79
|
+
const lines = generations.flatMap((p) => fs.readFileSync(p, "utf8").split("\n").filter(Boolean))
|
|
80
|
+
return { files: generations.map((p) => path.basename(p)), lines: lines.slice(-2000) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Accumulator for one parent session, seeded to the empty status state.
|
|
85
|
+
* @returns {Record<string, unknown>} Fresh session accumulator.
|
|
86
|
+
*/
|
|
87
|
+
function freshSession() {
|
|
88
|
+
return {
|
|
89
|
+
lastGateResult: null,
|
|
90
|
+
hasGatePass: false,
|
|
91
|
+
lastPlanResult: null,
|
|
92
|
+
hasPlanVerdict: false,
|
|
93
|
+
planApproval: { effective: null, verdict: null, source: null, promptApproval: false },
|
|
94
|
+
lastGate: { marker: "", ts: "" },
|
|
95
|
+
lastVerdict: { marker: "", ts: "" },
|
|
96
|
+
waivers: [],
|
|
97
|
+
dispatches: 0,
|
|
98
|
+
blocks: 0,
|
|
99
|
+
permissionAsks: 0,
|
|
100
|
+
permissionDecisions: { once: 0, always: 0, reject: 0 },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Aggregates the journal lines into per-session status. Replays the same
|
|
106
|
+
* gate/plan last-wins rules as state.ts's replayGatePasses: a trailing
|
|
107
|
+
* `GATE: FAIL` keeps the gate red, an enforce-mode `verified:false` PASS never
|
|
108
|
+
* flips, and a `type:plan` APPROVE restores plan approval. Malformed lines are
|
|
109
|
+
* skipped. Parent-keyed.
|
|
110
|
+
* @param {string[]} lines Journal lines, oldest-first.
|
|
111
|
+
* @returns {Map<string, Record<string, unknown>>} sessionID → accumulator.
|
|
112
|
+
*/
|
|
113
|
+
function aggregate(lines) {
|
|
114
|
+
const sessions = new Map()
|
|
115
|
+
const forSession = (id) => {
|
|
116
|
+
let a = sessions.get(id)
|
|
117
|
+
if (!a) {
|
|
118
|
+
a = freshSession()
|
|
119
|
+
sessions.set(id, a)
|
|
120
|
+
}
|
|
121
|
+
return a
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
let e
|
|
126
|
+
try {
|
|
127
|
+
e = JSON.parse(line)
|
|
128
|
+
} catch {
|
|
129
|
+
continue // malformed line: skip, replay-parity.
|
|
130
|
+
}
|
|
131
|
+
if (!e || typeof e !== "object" || Array.isArray(e)) continue
|
|
132
|
+
const sid = typeof e.sessionID === "string" && e.sessionID ? e.sessionID : ""
|
|
133
|
+
if (!sid) continue
|
|
134
|
+
const a = forSession(sid)
|
|
135
|
+
const type = e.type
|
|
136
|
+
const result = e.result
|
|
137
|
+
|
|
138
|
+
if (type === "gate") {
|
|
139
|
+
// Gate flags restored exactly like replayGatePasses: last flipping result
|
|
140
|
+
// wins; an enforce-mode unverified PASS neither flips here nor at runtime.
|
|
141
|
+
if (result === "PASS" || result === "FAIL") {
|
|
142
|
+
const flips = !(result === "PASS" && e.verified === false && e.mode !== "flag")
|
|
143
|
+
if (flips) {
|
|
144
|
+
a.lastGateResult = result
|
|
145
|
+
a.hasGatePass = result === "PASS"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (result === "WAIVED") {
|
|
149
|
+
a.waivers.push({
|
|
150
|
+
channel: typeof e.channel === "string" ? e.channel : "",
|
|
151
|
+
waiver: typeof e.waiver === "string" ? e.waiver : "",
|
|
152
|
+
})
|
|
153
|
+
} else if (typeof e.marker === "string" && e.marker) {
|
|
154
|
+
a.lastGate.marker = e.marker
|
|
155
|
+
a.lastGate.ts = typeof e.ts === "string" ? e.ts : ""
|
|
156
|
+
}
|
|
157
|
+
} else if (type === "plan") {
|
|
158
|
+
// Plan flags restored like replayGatePasses: last APPROVE/BLOCK wins.
|
|
159
|
+
if (result === "APPROVE" || result === "BLOCK") {
|
|
160
|
+
a.lastPlanResult = result
|
|
161
|
+
a.hasPlanVerdict = result === "APPROVE"
|
|
162
|
+
a.planApproval.effective = result
|
|
163
|
+
a.planApproval.verdict = result
|
|
164
|
+
a.planApproval.source = typeof e.source === "string" ? e.source : null
|
|
165
|
+
// A `PLAN APPROVED:` in the user prompt is journaled as a type:plan
|
|
166
|
+
// APPROVE (source "session-prompt"); replay restores it like any plan
|
|
167
|
+
// APPROVE above, and the status display surfaces the human provenance.
|
|
168
|
+
if (result === "APPROVE" && e.source === "session-prompt") {
|
|
169
|
+
a.planApproval.promptApproval = true
|
|
170
|
+
}
|
|
171
|
+
} else if (result === "WAIVED") {
|
|
172
|
+
a.waivers.push({ channel: typeof e.channel === "string" ? e.channel : "", waiver: "" })
|
|
173
|
+
}
|
|
174
|
+
} else if (type === "verdict") {
|
|
175
|
+
// VERDICT: APPROVE/BLOCK observations (plan-reviewer verdict markers).
|
|
176
|
+
const marker = typeof e.marker === "string" && e.marker ? e.marker : ""
|
|
177
|
+
if (marker) {
|
|
178
|
+
a.lastVerdict.marker = marker
|
|
179
|
+
a.lastVerdict.ts = typeof e.ts === "string" ? e.ts : ""
|
|
180
|
+
}
|
|
181
|
+
if (marker.includes("APPROVE")) {
|
|
182
|
+
a.planApproval.effective = "APPROVE"
|
|
183
|
+
a.planApproval.verdict = "APPROVE"
|
|
184
|
+
} else {
|
|
185
|
+
a.planApproval.effective = "BLOCK"
|
|
186
|
+
a.planApproval.verdict = "BLOCK"
|
|
187
|
+
}
|
|
188
|
+
a.planApproval.source = typeof e.source === "string" ? e.source : a.planApproval.source
|
|
189
|
+
} else if (type === "dispatch") {
|
|
190
|
+
a.dispatches++
|
|
191
|
+
} else if (type === "blocked") {
|
|
192
|
+
a.blocks++
|
|
193
|
+
} else if (type === "permission") {
|
|
194
|
+
if (e.action === "ask") {
|
|
195
|
+
a.permissionAsks++
|
|
196
|
+
} else if (e.decision === "once" || e.decision === "always" || e.decision === "reject") {
|
|
197
|
+
a.permissionDecisions[e.decision]++
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return sessions
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Truncates a session ID for a table cell (JSON keeps the full ID).
|
|
207
|
+
* @param {string} id The raw session ID.
|
|
208
|
+
* @returns {string} The display ID.
|
|
209
|
+
*/
|
|
210
|
+
function fmtSession(id) {
|
|
211
|
+
return id.length > 12 ? `${id.slice(0, 9)}…` : id
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Renders the human-readable table.
|
|
216
|
+
* @param {Map<string, Record<string, unknown>>} sessions Aggregated sessions.
|
|
217
|
+
* @param {string} journalPath The journal file path for the header.
|
|
218
|
+
* @returns {string} The table text.
|
|
219
|
+
*/
|
|
220
|
+
function renderTable(sessions, journalPath) {
|
|
221
|
+
const rows = [...sessions.entries()].map(([id, a]) => ({
|
|
222
|
+
session: fmtSession(id),
|
|
223
|
+
gate: a.hasGatePass ? "PASS" : "red",
|
|
224
|
+
plan: a.hasPlanVerdict ? "APPROVE" : (a.planApproval.effective ?? "·"),
|
|
225
|
+
verdict: a.planApproval.effective ?? "·",
|
|
226
|
+
wv: a.waivers.length,
|
|
227
|
+
dis: a.dispatches,
|
|
228
|
+
bl: a.blocks,
|
|
229
|
+
ask: a.permissionAsks,
|
|
230
|
+
dec: a.permissionDecisions.once + a.permissionDecisions.always + a.permissionDecisions.reject,
|
|
231
|
+
}))
|
|
232
|
+
|
|
233
|
+
const headers = ["SESSION", "GATE", "PLAN", "VERDICT", "WV", "DIS", "BLK", "PERM"]
|
|
234
|
+
const cells = [
|
|
235
|
+
(r) => r.session,
|
|
236
|
+
(r) => r.gate,
|
|
237
|
+
(r) => r.plan,
|
|
238
|
+
(r) => r.verdict,
|
|
239
|
+
(r) => String(r.wv),
|
|
240
|
+
(r) => String(r.dis),
|
|
241
|
+
(r) => String(r.bl),
|
|
242
|
+
(r) => `${r.ask}/${r.dec}`,
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => cells[i](r).length)))
|
|
246
|
+
const line = (vals) =>
|
|
247
|
+
vals
|
|
248
|
+
.map((v, i) => v.padEnd(widths[i]))
|
|
249
|
+
.join(" ")
|
|
250
|
+
.replace(/\s+$/, "")
|
|
251
|
+
|
|
252
|
+
const out = [`turnstile status — ${journalPath}`]
|
|
253
|
+
if (rows.length === 0) {
|
|
254
|
+
out.push("(no sessions recorded yet)")
|
|
255
|
+
} else {
|
|
256
|
+
out.push(line(headers))
|
|
257
|
+
for (const r of rows) out.push(line(cells.map((c) => c(r))))
|
|
258
|
+
}
|
|
259
|
+
out.push("", "journal reflects the last prompt-cycle; in-memory state can be fresher.")
|
|
260
|
+
return out.join("\n")
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Parses the status arguments. Recognizes --json and --help; anything else is
|
|
265
|
+
* an unknown flag and fails with exit code 2.
|
|
266
|
+
* @param {string[]} argv Arguments after the `status` subcommand.
|
|
267
|
+
* @returns {{ json: boolean, help: boolean }} Parsed flags.
|
|
268
|
+
*/
|
|
269
|
+
function parseArgs(argv) {
|
|
270
|
+
const json = argv.includes("--json")
|
|
271
|
+
const help = argv.includes("--help")
|
|
272
|
+
for (const a of argv) {
|
|
273
|
+
if (a === "--json" || a === "--help") continue
|
|
274
|
+
throw new Error(`unknown argument "${a}"`)
|
|
275
|
+
}
|
|
276
|
+
return { json, help }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Entry point. Parses args, resolves the journal, aggregates, and prints.
|
|
281
|
+
* @param {string[]} argv Arguments after the `status` subcommand.
|
|
282
|
+
* @returns {Promise<number>} The process exit code.
|
|
283
|
+
*/
|
|
284
|
+
async function main(argv) {
|
|
285
|
+
let flags
|
|
286
|
+
try {
|
|
287
|
+
flags = parseArgs(argv)
|
|
288
|
+
} catch (e) {
|
|
289
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
290
|
+
process.stderr.write(`${msg}\n`)
|
|
291
|
+
process.stderr.write("usage: opencode-turnstile status [--json]\n")
|
|
292
|
+
return 2
|
|
293
|
+
}
|
|
294
|
+
if (flags.help) {
|
|
295
|
+
process.stdout.write(`${USAGE}\n`)
|
|
296
|
+
return 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const metricsDir = resolveMetricsDir(process.env)
|
|
300
|
+
const journalPath = path.join(metricsDir, "turnstile.jsonl")
|
|
301
|
+
const { files, lines } = readJournal(journalPath)
|
|
302
|
+
|
|
303
|
+
const base = {
|
|
304
|
+
journal: journalPath,
|
|
305
|
+
files,
|
|
306
|
+
noJournal: files.length === 0,
|
|
307
|
+
sessions: {},
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (files.length === 0) {
|
|
311
|
+
// Read-only no-op: nothing recorded yet.
|
|
312
|
+
if (flags.json) {
|
|
313
|
+
process.stdout.write(`${JSON.stringify(base, null, 2)}\n`)
|
|
314
|
+
} else {
|
|
315
|
+
process.stdout.write(`No turnstile journal yet at ${journalPath} — nothing to report.\n`)
|
|
316
|
+
}
|
|
317
|
+
return 0
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const sessions = aggregate(lines)
|
|
321
|
+
for (const [id, a] of sessions) {
|
|
322
|
+
base.sessions[id] = a
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (flags.json) {
|
|
326
|
+
process.stdout.write(`${JSON.stringify(base, null, 2)}\n`)
|
|
327
|
+
} else {
|
|
328
|
+
process.stdout.write(`${renderTable(sessions, journalPath)}\n`)
|
|
329
|
+
}
|
|
330
|
+
return 0
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
main(process.argv.slice(2)).then((code) => process.exit(code ?? 0))
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: turnstile-setup
|
|
3
|
-
description: Install
|
|
3
|
+
description: Install the turnstile plugin and pipeline agents into the user's ~/.config/opencode and verify sync. Use when the user asks to set up turnstile, run the pipeline gates, add pipeline gates to a session, or after changing marker syntax in this repo. The plugin injects the orchestrator policy at runtime (via the execute.after hook); it does not write a contract block into AGENTS.md.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Turnstile setup
|
|
7
7
|
|
|
8
|
-
Install the artifacts this repo owns into `~/.config/opencode` — the
|
|
9
|
-
|
|
10
|
-
`install.json`)
|
|
11
|
-
user's global
|
|
8
|
+
Install the artifacts this repo owns into `~/.config/opencode` — the plugin and
|
|
9
|
+
the pipeline agent definitions — as symlinks (`scripts/install.mjs`, driven by
|
|
10
|
+
`install.json`). This repo no longer owns a contract block: the orchestrator
|
|
11
|
+
policy is injected at runtime by the plugin, not written to the user's global
|
|
12
|
+
instructions file (`~/.config/opencode/AGENTS.md`) at install time.
|
|
12
13
|
|
|
13
14
|
## Steps
|
|
14
15
|
|
|
@@ -28,49 +29,32 @@ user's global instructions file (`scripts/install-agents.mjs`).
|
|
|
28
29
|
- Verify with `npm run install:check` (add `-- --copy` for copy mode)
|
|
29
30
|
— must exit 0.
|
|
30
31
|
|
|
31
|
-
2.
|
|
32
|
+
2. Confirm the runtime policy. The plugin injects the orchestrator policy
|
|
33
|
+
into the primary session per task from `src/policy.ts`; there is no
|
|
34
|
+
block to verify on disk. If the user's `~/.config/opencode/AGENTS.md`
|
|
35
|
+
still contains a legacy turnstile sentinel block (from an install before
|
|
36
|
+
the runtime-injection change), delete it by hand — the plugin does not
|
|
37
|
+
read or manage it, and there is deliberately no installer to remove it:
|
|
32
38
|
|
|
33
39
|
```
|
|
34
|
-
|
|
40
|
+
# ~/.config/opencode/AGENTS.md — delete the section between the
|
|
41
|
+
# `<!-- turnstile:start -->` and `<!-- turnstile:end -->` sentinels
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
`--file <path>` only if the user asks for a different file.
|
|
39
|
-
- Preflight: adoption is automatic. When the target already contains
|
|
40
|
-
turnstile contract content but **no** `<!-- turnstile:start -->` …
|
|
41
|
-
`<!-- turnstile:end -->` sentinels, the installer adopts the section:
|
|
42
|
-
it wraps an equivalent section with the sentinels (no content
|
|
43
|
-
change), and for a section that differs it backs up the target first
|
|
44
|
-
(`<file>.bak.<timestamp>`) and replaces the span with the current
|
|
45
|
-
template. Run with `--check` to preview without writing, or
|
|
46
|
-
`--no-adopt` to refuse and get reconciliation instructions instead.
|
|
47
|
-
- The installer only refuses (exit 1, no write) for targets it cannot
|
|
48
|
-
resolve safely: a corrupted one — a start sentinel with no end
|
|
49
|
-
sentinel — or an ambiguous one — more than one turnstile section.
|
|
50
|
-
Fix one of those by hand, then re-run.
|
|
51
|
-
- The script touches only the managed block; everything else in the
|
|
52
|
-
target file is preserved.
|
|
53
|
-
|
|
54
|
-
3. Verify the installed block matches the template:
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
node scripts/install-agents.mjs --check
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Must exit 0. If it exits 1, re-run step 2 and investigate rather than
|
|
61
|
-
hand-editing the installed file.
|
|
62
|
-
|
|
63
|
-
4. Verify the marker contract is still in sync with the parser:
|
|
44
|
+
3. Verify the marker contract is still in sync with the parser:
|
|
64
45
|
|
|
65
46
|
```
|
|
66
47
|
node scripts/check-markers.mjs
|
|
67
48
|
```
|
|
68
49
|
|
|
69
|
-
Must pass.
|
|
50
|
+
Must pass. It validates the README's concrete examples, the `<n>`-placeholder
|
|
51
|
+
templates in `agents/*.md`, and those in `src/policy.ts` against the parser
|
|
52
|
+
regexes in `src/markers.ts`. If it fails, the README examples and the regex
|
|
70
53
|
literals in `src/markers.ts` have drifted — fix both together.
|
|
71
54
|
|
|
72
55
|
## Report
|
|
73
56
|
|
|
74
|
-
Tell the user: which files were linked or
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
Tell the user: which files were linked or copied (created or updated), that the
|
|
58
|
+
orchestrator policy is injected at runtime (no instructions-file block), and —
|
|
59
|
+
if a legacy sentinel block was present — that it was removed by hand. Do not
|
|
60
|
+
quote the injected policy back; summarize.
|