@kontourai/flow-agents 3.7.0 → 3.8.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.
@@ -915,7 +915,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
915
915
  if (!check.id) continue;
916
916
  const subjectId = `${slug}/${check.id}`;
917
917
  const fieldOrBehavior = String(check.summary ?? check.id);
918
- const claimId = generateClaimId(subjectId, "flow-agents.workflow", fieldOrBehavior);
918
+ const gateClaimIdentityVersion = check._gate_claim_identity_version === 2 ? 2 : 1;
919
+ const gateClaimRecordedAt = gateClaimIdentityVersion === 2 && typeof check._gate_claim_recorded_at === "string" ? check._gate_claim_recorded_at : null;
920
+ const claimId = generateClaimId(subjectId, "flow-agents.workflow", gateClaimRecordedAt ? `${fieldOrBehavior}::gate-visit::${gateClaimRecordedAt}` : fieldOrBehavior);
919
921
  const evId = `ev:${claimId}`;
920
922
  const legacyClaimType = `workflow.check.${check.kind ?? "external"}`;
921
923
 
@@ -1071,7 +1073,7 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
1071
1073
  // restored stamp) takes the currently-active step's id.
1072
1074
  const declaredStepId = gateClaimDeclaredStepId ?? (activeStep ? activeStep.stepId : null);
1073
1075
  const declaredMetadata: AnyObj = gateClaimExpectationId
1074
- ? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
1076
+ ? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimIdentityVersion === 2 ? { identity_version: 2 } : {}), ...(gateClaimRecordedAt ? { recorded_at: gateClaimRecordedAt } : {}), ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
1075
1077
  : claimMetadata;
1076
1078
  const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: effectiveStatus, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, ...(declaredMetadata ? { metadata: declaredMetadata } : {}) };
1077
1079
  const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [evItem] as Record<string, unknown>[], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
@@ -1089,7 +1091,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
1089
1091
  if (!criterion.id) continue;
1090
1092
  const subjectId = `${slug}/${criterion.id}`;
1091
1093
  const fieldOrBehavior = String(criterion.description ?? criterion.id);
1092
- const claimId = generateClaimId(subjectId, "flow-agents.workflow", fieldOrBehavior);
1094
+ const criterionIdentityVersion = criterion.identity_version === 2 ? 2 : 1;
1095
+ const criterionVerifiedAt = criterionIdentityVersion === 2 && typeof criterion.verified_at === "string" ? criterion.verified_at : null;
1096
+ const claimId = generateClaimId(subjectId, "flow-agents.workflow", criterionVerifiedAt ? `${fieldOrBehavior}::verified::${criterionVerifiedAt}` : fieldOrBehavior);
1093
1097
  const legacyClaimType = "workflow.acceptance.criterion";
1094
1098
  const policy = ensurePolicy(legacyClaimType, "high", []);
1095
1099
  const evStatus = criterionStatusToEventStatus(String(criterion.status ?? ""));
@@ -1105,7 +1109,7 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
1105
1109
  if (declared) {
1106
1110
  // Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
1107
1111
  const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
1108
- const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [] }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
1112
+ const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [], ...(criterionIdentityVersion === 2 ? { identity_version: 2 } : {}), ...(criterionVerifiedAt ? { verified_at: criterionVerifiedAt } : {}) }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
1109
1113
  const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
1110
1114
  claims.push({ ...declaredClaimObj, status: declaredStatus });
1111
1115
  } else {
@@ -1128,10 +1132,12 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
1128
1132
  const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
1129
1133
  const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
1130
1134
  const critiqueReviewedAt = String(c.reviewed_at ?? ts);
1135
+ const critiqueIdentityVersion = c.identity_version === 2 ? 2 : 1;
1131
1136
  const critMeta: AnyObj = {
1132
1137
  origin: "critique",
1133
1138
  reviewer: critiqueReviewer,
1134
1139
  reviewed_at: critiqueReviewedAt,
1140
+ ...(critiqueIdentityVersion === 2 ? { identity_version: 2 } : {}),
1135
1141
  findings: Array.isArray(c.findings) ? c.findings : [],
1136
1142
  lanes: Array.isArray(c.lanes) ? c.lanes : [],
1137
1143
  review_target: c.review_target && typeof c.review_target === "object" ? c.review_target : { artifacts: [] },
@@ -1143,7 +1149,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
1143
1149
  // A superseded historical write gets a distinct, stable claimId so it co-exists with the live
1144
1150
  // claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
1145
1151
  // across rebuilds because superseded_by + reviewed_at are preserved in metadata.
1146
- const claimIdSalt = supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior;
1152
+ const claimIdSalt = critiqueIdentityVersion === 2
1153
+ ? `${fieldOrBehavior}::reviewed::${critiqueReviewedAt}${supersededBy ? `::superseded::${supersededBy}` : ""}`
1154
+ : (supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior);
1147
1155
  const claimId = generateClaimId(subjectId, "flow-agents.workflow", claimIdSalt);
1148
1156
  const legacyClaimType = "workflow.critique.review";
1149
1157
  const policy = ensurePolicy(legacyClaimType, "medium", []);
@@ -2727,7 +2735,7 @@ function requireObservedCommandRefs(refs: AnyObj[], observedCommands: ReadonlySe
2727
2735
  }
2728
2736
  }
2729
2737
 
2730
- function completePassingCriteria(existing: AnyObj[], raw: string[], observedCommands: ReadonlySet<string>): AnyObj[] {
2738
+ function completePassingCriteria(existing: AnyObj[], raw: string[], observedCommands: ReadonlySet<string>, verifiedAt: string): AnyObj[] {
2731
2739
  if (raw.length === 0) die("record-gate-claim requires --criterion-json for a passing tests-evidence claim");
2732
2740
  const incoming = raw.map((value) => parseJson(value, "--criterion-json"));
2733
2741
  const expectedById = new Map<string, AnyObj>();
@@ -2746,7 +2754,7 @@ function completePassingCriteria(existing: AnyObj[], raw: string[], observedComm
2746
2754
  const refs = normalizeEvidenceRefs(criterion.evidence_refs, `criterion ${ids[index]} evidence_refs`);
2747
2755
  if (refs.length === 0) die(`criterion ${ids[index]} requires reviewable evidence_refs`);
2748
2756
  requireObservedCommandRefs(refs, observedCommands, `criterion ${ids[index]}`);
2749
- return { ...expectedById.get(ids[index])!, status: "pass", evidence_refs: refs };
2757
+ return { ...expectedById.get(ids[index])!, status: "pass", evidence_refs: refs, identity_version: 2, verified_at: verifiedAt };
2750
2758
  });
2751
2759
  }
2752
2760
 
@@ -3133,6 +3141,8 @@ function checksFromBundle(dir: string): AnyObj[] {
3133
3141
  if (typeof gc.claim_type === "string") check._gate_claim_declared_type = gc.claim_type;
3134
3142
  if (typeof gc.subject_type === "string") check._gate_claim_declared_subject = gc.subject_type;
3135
3143
  if (typeof gc.step_id === "string") check._gate_claim_declared_step_id = gc.step_id;
3144
+ if (typeof gc.recorded_at === "string") check._gate_claim_recorded_at = gc.recorded_at;
3145
+ if (gc.identity_version === 2) check._gate_claim_identity_version = 2;
3136
3146
  if (typeof gc.route_reason === "string") check._gate_claim_route_reason = gc.route_reason;
3137
3147
  };
3138
3148
  // #270 CRITICAL/HIGH fix: a claim that is gate-claim-SHAPED but carries NO metadata.gate_claim
@@ -3294,6 +3304,7 @@ function critiquesFromBundle(dir: string): AnyObj[] {
3294
3304
  review_target: md.review_target && typeof md.review_target === "object" && !Array.isArray(md.review_target) ? md.review_target : { artifacts: [] },
3295
3305
  reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
3296
3306
  reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
3307
+ ...(md.identity_version === 2 ? { identity_version: 2 } : {}),
3297
3308
  artifact_refs: Array.isArray(md.artifact_refs) ? md.artifact_refs : [],
3298
3309
  ...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
3299
3310
  };
@@ -3314,6 +3325,8 @@ function criteriaFromBundle(dir: string): AnyObj[] {
3314
3325
  description: typeof saved.description === "string" ? saved.description : (claim.fieldOrBehavior || ""),
3315
3326
  status: typeof saved.status === "string" ? saved.status : (claim.value ?? "not_verified"),
3316
3327
  evidence_refs: Array.isArray(saved.evidence_refs) ? saved.evidence_refs : [],
3328
+ ...(typeof saved.verified_at === "string" ? { verified_at: saved.verified_at } : {}),
3329
+ ...(saved.identity_version === 2 ? { identity_version: 2 } : {}),
3317
3330
  };
3318
3331
  })
3319
3332
  .filter((criterion: AnyObj) => typeof criterion.id === "string" && criterion.id.length > 0);
@@ -3369,7 +3382,7 @@ async function recordEvidence(p: ReturnType<typeof parseArgs>): Promise<number>
3369
3382
  if (!checks.length && opts(p, "surface-trust-json").length === 0) die("record-evidence requires at least one --check-json or --surface-trust-json");
3370
3383
  validateAcceptanceEvidenceRefs(dir, p);
3371
3384
  // Phase 4c: bundle is the sole verification artifact — stop writing evidence.json and acceptance.json update.
3372
- const ts = opt(p, "timestamp", now());
3385
+ const ts = opt(p, "timestamp", new Date().toISOString());
3373
3386
  // #298: readBundleState + merge-by-id instead of unconditionally replacing every prior check —
3374
3387
  // record-evidence previously never called checksFromBundle(dir), so it dropped every check
3375
3388
  // recorded by an earlier record-evidence/record-check/record-gate-claim call. A later check
@@ -3576,7 +3589,7 @@ function diagnostic(dir: string, code: string, summary: string): never {
3576
3589
  async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number> {
3577
3590
  const dir = artifactDirFrom(p.positional[0] || die("artifact directory is required"));
3578
3591
  const slug = taskSlugFor(dir, opt(p, "task-slug"));
3579
- const ts = opt(p, "timestamp", now());
3592
+ const ts = opt(p, "timestamp", new Date().toISOString());
3580
3593
  const statusVal = opt(p, "status");
3581
3594
  if (!["pass", "fail", "not_verified"].includes(statusVal)) die("--status must be one of: pass, fail, not_verified");
3582
3595
  const summary = opt(p, "summary") || die("--summary is required");
@@ -3652,6 +3665,8 @@ async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number>
3652
3665
  status: statusVal,
3653
3666
  summary,
3654
3667
  _gate_claim_expectation_id: targetExpectation.id,
3668
+ _gate_claim_identity_version: 2,
3669
+ _gate_claim_recorded_at: ts,
3655
3670
  ...(routeReason ? { _gate_claim_route_reason: routeReason } : {}),
3656
3671
  };
3657
3672
 
@@ -3690,7 +3705,7 @@ async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number>
3690
3705
  // SAME expectation id supersedes the earlier check for that expectation (mergeChecksById); a
3691
3706
  // gate claim against a different expectation is additive.
3692
3707
  const _existingState = readBundleState(dir);
3693
- const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames) : _existingState.criteria;
3708
+ const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames, ts) : _existingState.criteria;
3694
3709
  if (mustRunTests) {
3695
3710
  const liveCritiques = _existingState.critiques.filter((critique) => !critique.superseded_by);
3696
3711
  if (liveCritiques.length === 0 || liveCritiques.some((critique) => !critiqueIsCleanAndCurrent(dir, critique))) {
@@ -3907,7 +3922,8 @@ async function recordCritique(p: ReturnType<typeof parseArgs>): Promise<number>
3907
3922
  const critique = {
3908
3923
  id: critiqueId,
3909
3924
  reviewer: opt(p, "reviewer", "tool-code-reviewer"),
3910
- reviewed_at: opt(p, "timestamp", now()),
3925
+ reviewed_at: opt(p, "timestamp", new Date().toISOString()),
3926
+ identity_version: 2,
3911
3927
  verdict,
3912
3928
  summary,
3913
3929
  lanes,
@@ -193,7 +193,7 @@ async function evidence(sessionDir: string, argv: string[], json: boolean): Prom
193
193
  // session state cannot change mid-invocation.
194
194
  const caller = assertMatchingAssignmentActor(sessionDir, slug);
195
195
  const repaired = await recoverBuilderFlowSession({ sessionDir });
196
- const beforeEvidence = manifestEvidenceDigests(repaired.run.manifest);
196
+ const beforeEvidence = manifestEvidenceIdentity(repaired.run.manifest);
197
197
  await mainFromPublicWorkflow([
198
198
  "record-gate-claim",
199
199
  sessionDir,
@@ -202,26 +202,33 @@ async function evidence(sessionDir: string, argv: string[], json: boolean): Prom
202
202
  caller.actorKey,
203
203
  ]);
204
204
 
205
- await syncBuilderFlowSession({ sessionDir });
205
+ const synchronized = await syncBuilderFlowSession({ sessionDir });
206
206
 
207
207
  const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
208
208
  const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
209
- const afterEvidence = manifestEvidenceDigests(run.manifest);
210
- const newEvidence = afterEvidence.filter((value) => !beforeEvidence.includes(value));
211
- if (newEvidence.length !== 1 || newEvidence[0] !== digest) {
209
+ const afterEvidence = manifestEvidenceIdentity(run.manifest);
210
+ const beforeIds = new Set(beforeEvidence.map((entry) => entry.id));
211
+ const newEvidence = afterEvidence.filter((entry) => !beforeIds.has(entry.id));
212
+ if (synchronized.attached && (newEvidence.length !== 1 || newEvidence[0]?.sha256 !== digest)) {
212
213
  throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
213
214
  }
215
+ if (!synchronized.attached && newEvidence.length !== 0) {
216
+ throw new Error("workflow evidence changed the canonical manifest while synchronization reported no attachment");
217
+ }
214
218
  const updatedSidecar = readBoundSession(sessionDir).sidecar;
215
219
  return immutableReport({
216
220
  run_id: run.runId,
217
221
  status: run.state.status,
218
222
  current_step: run.state.current_step,
219
- attached: true,
223
+ attached: synchronized.attached,
224
+ awaiting_evidence: !synchronized.attached,
220
225
  next_action: updatedSidecar.next_action ?? null,
221
226
  });
222
227
  });
223
228
  if (json) console.log(JSON.stringify(report));
224
- else console.log(`Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`);
229
+ else console.log(report.attached
230
+ ? `Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`
231
+ : `Recorded evidence; canonical run is awaiting the remaining gate expectations at ${report.current_step}.`);
225
232
  return 0;
226
233
  }
227
234
 
@@ -511,11 +518,14 @@ function immutableReport<T>(value: T): T {
511
518
  return Object.freeze(value);
512
519
  }
513
520
 
514
- function manifestEvidenceDigests(manifest: JsonRecord): string[] {
521
+ function manifestEvidenceIdentity(manifest: JsonRecord): Array<{ id: string; sha256: string }> {
515
522
  const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
516
523
  return evidence
517
- .map((entry) => entry && typeof entry === "object" ? (entry as JsonRecord).sha256 : null)
518
- .filter((digest): digest is string => typeof digest === "string");
524
+ .flatMap((entry) => entry && typeof entry === "object"
525
+ && typeof (entry as JsonRecord).id === "string"
526
+ && typeof (entry as JsonRecord).sha256 === "string"
527
+ ? [{ id: String((entry as JsonRecord).id), sha256: String((entry as JsonRecord).sha256) }]
528
+ : []);
519
529
  }
520
530
 
521
531
  function optionalFileDigest(file: string): string | null {
@@ -133,6 +133,54 @@ function copyTree(src: string, dest: string, target: string, rootReplacement: st
133
133
  }
134
134
  }
135
135
 
136
+ const skillResourcePattern = /(?:^|[`("'\s])(context\/contracts\/[A-Za-z0-9._/-]+\.md)(?=$|[`),"'\s])/gm;
137
+
138
+ /** Export a skill as a self-contained package. Shared instruction resources
139
+ * retain the relative paths used by SKILL.md so runtime resolution is local. */
140
+ function exportSkillPackage(src: string, dest: string, target: string): void {
141
+ const sourceDir = path.dirname(src);
142
+ copyTree(sourceDir, dest, target, "<bundle-root>");
143
+ const skillText = readText(src);
144
+ const resources = new Set<string>();
145
+ for (const match of skillText.matchAll(skillResourcePattern)) resources.add(match[1]);
146
+ for (const rel of [...resources].sort()) {
147
+ if (path.isAbsolute(rel) || rel.split("/").includes("..")) {
148
+ throw new Error(`skill '${path.basename(sourceDir)}': unsafe local resource '${rel}'`);
149
+ }
150
+ const source = path.resolve(root, rel);
151
+ const output = path.resolve(dest, rel);
152
+ if (!source.startsWith(`${path.resolve(root)}${path.sep}`) || !output.startsWith(`${path.resolve(dest)}${path.sep}`)) {
153
+ throw new Error(`skill '${path.basename(sourceDir)}': local resource escapes package '${rel}'`);
154
+ }
155
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
156
+ throw new Error(`skill '${path.basename(sourceDir)}': missing local resource '${rel}'`);
157
+ }
158
+ if (fs.existsSync(output)) {
159
+ const existing = fs.readFileSync(output);
160
+ const incoming = fs.readFileSync(source);
161
+ if (!existing.equals(incoming)) throw new Error(`skill '${path.basename(sourceDir)}': resource collision '${rel}'`);
162
+ continue;
163
+ }
164
+ writeText(output, sanitizeText(readText(source), target, "<bundle-root>"));
165
+ }
166
+ }
167
+
168
+ function validateExportedSkillPackages(skillsRoot: string): void {
169
+ if (!fs.existsSync(skillsRoot)) return;
170
+ for (const skillName of fs.readdirSync(skillsRoot).sort()) {
171
+ const skillDir = path.join(skillsRoot, skillName);
172
+ const skillFile = path.join(skillDir, "SKILL.md");
173
+ if (!fs.existsSync(skillFile)) continue;
174
+ for (const match of readText(skillFile).matchAll(skillResourcePattern)) {
175
+ const rel = match[1];
176
+ const resolved = path.resolve(skillDir, rel);
177
+ if (!resolved.startsWith(`${path.resolve(skillDir)}${path.sep}`) || !fs.existsSync(resolved)) {
178
+ throw new Error(`skill '${skillName}': unresolved exported resource '${rel}'`);
179
+ }
180
+ }
181
+ }
182
+ }
183
+
136
184
  function resolveSourcePath(pathText: string): string {
137
185
  let normalized = pathText;
138
186
  for (const alias of manifest.source_root_aliases) normalized = normalized.split(alias).join(root);
@@ -440,9 +488,9 @@ function buildCodex(agents: Agent[]): void {
440
488
  for (const [profileName, profile] of Object.entries(manifest.codex.profiles ?? {})) writeText(path.join(bundle, ".codex", `${profileName}.config.toml`), exportCodexProfileConfig(profile as Record<string, unknown>, settings));
441
489
  writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
442
490
  for (const spec of targetAgents) writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
443
- for (const { name, src } of collectAllSkills()) {
444
- writeText(path.join(bundle, ".codex/skills", name, "SKILL.md"), sanitizeText(readText(src), "codex", "<bundle-root>"));
445
- }
491
+ const skillsRoot = path.join(bundle, ".agents/skills");
492
+ for (const { name, src } of collectAllSkills()) exportSkillPackage(src, path.join(skillsRoot, name), "codex");
493
+ validateExportedSkillPackages(skillsRoot);
446
494
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
447
495
  writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace", CODEX_LIVE_HOOKS_README));
448
496
  writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace", undefined, undefined, { configRelPath: ".codex/hooks.json", managedConfigRelPath: ".codex/hooks.json", runtime: "codex", version: pkgVersion }));