@open-agent-toolkit/cli 0.3.7 → 0.3.9

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 (147) hide show
  1. package/README.md +3 -3
  2. package/assets/agents/oat-codebase-mapper.md +3 -2
  3. package/assets/agents/oat-reviewer.md +5 -13
  4. package/assets/agents/skeptical-evaluator.md +6 -2
  5. package/assets/bundle-metadata.json +1 -1
  6. package/assets/docs/cli-utilities/backlog-lifecycle.md +3 -2
  7. package/assets/docs/cli-utilities/bootstrap.md +5 -2
  8. package/assets/docs/cli-utilities/config-and-local-state.md +13 -6
  9. package/assets/docs/cli-utilities/configuration.md +25 -10
  10. package/assets/docs/cli-utilities/tool-packs.md +60 -10
  11. package/assets/docs/cli-utilities/workflow-gates.md +37 -0
  12. package/assets/docs/contributing/code.md +9 -5
  13. package/assets/docs/provider-sync/commands.md +17 -8
  14. package/assets/docs/provider-sync/instruction-sync.md +241 -39
  15. package/assets/docs/provider-sync/providers.md +20 -0
  16. package/assets/docs/provider-sync/scope-and-surface.md +3 -1
  17. package/assets/docs/reference/cli-reference.md +4 -1
  18. package/assets/docs/reference/oat-directory-structure.md +12 -11
  19. package/assets/docs/reference/troubleshooting.md +19 -4
  20. package/assets/docs/workflows/projects/evidence-layers.md +5 -1
  21. package/assets/docs/workflows/projects/implementation-execution.md +22 -8
  22. package/assets/docs/workflows/projects/lifecycle.md +4 -6
  23. package/assets/docs/workflows/projects/retro.md +7 -0
  24. package/assets/public-package-versions.json +4 -4
  25. package/assets/skills/oat-agent-instructions-analyze/SKILL.md +4 -4
  26. package/assets/skills/oat-agent-instructions-analyze/references/analysis-artifact-template.md +5 -5
  27. package/assets/skills/oat-agent-instructions-analyze/references/docs/rules-files.md +14 -11
  28. package/assets/skills/oat-agent-instructions-analyze/references/quality-checklist.md +2 -2
  29. package/assets/skills/oat-agent-instructions-analyze/scripts/resolve-instruction-files.sh +4 -3
  30. package/assets/skills/oat-agent-instructions-analyze/scripts/resolve-providers.sh +17 -6
  31. package/assets/skills/oat-agent-instructions-apply/SKILL.md +3 -3
  32. package/assets/skills/oat-agent-instructions-apply/references/docs/rules-files.md +14 -11
  33. package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
  34. package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +387 -0
  35. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +75 -0
  36. package/assets/skills/oat-doctor/SKILL.md +17 -13
  37. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  38. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  39. package/assets/skills/oat-project-implement/SKILL.md +2 -2
  40. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +33 -11
  41. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +24 -22
  42. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  43. package/assets/skills/oat-project-implement/references/phase-execution.md +109 -8
  44. package/assets/skills/oat-project-lite/SKILL.md +23 -1
  45. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
  46. package/assets/skills/oat-project-next/SKILL.md +3 -3
  47. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  48. package/assets/skills/oat-project-progress/SKILL.md +2 -2
  49. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  50. package/assets/skills/oat-project-retro/SKILL.md +70 -6
  51. package/assets/skills/oat-project-review-provide/SKILL.md +3 -1
  52. package/assets/skills/oat-project-review-provide-remote/SKILL.md +3 -1
  53. package/assets/templates/docs-app-mkdocs/docs/contributing.md +1 -3
  54. package/assets/templates/pjm-agents.md +7 -3
  55. package/assets/templates/state.md +2 -2
  56. package/dist/agents/canonical/resolve.d.ts +6 -4
  57. package/dist/agents/canonical/resolve.d.ts.map +1 -1
  58. package/dist/agents/canonical/resolve.js +6 -4
  59. package/dist/commands/backlog/archive.d.ts +10 -2
  60. package/dist/commands/backlog/archive.d.ts.map +1 -1
  61. package/dist/commands/backlog/archive.js +22 -6
  62. package/dist/commands/backlog/index.d.ts.map +1 -1
  63. package/dist/commands/backlog/index.js +5 -1
  64. package/dist/commands/backlog/rewrite-references.d.ts +41 -0
  65. package/dist/commands/backlog/rewrite-references.d.ts.map +1 -0
  66. package/dist/commands/backlog/rewrite-references.js +516 -0
  67. package/dist/commands/config/index.d.ts +1 -0
  68. package/dist/commands/config/index.d.ts.map +1 -1
  69. package/dist/commands/config/index.js +69 -34
  70. package/dist/commands/gate/index.d.ts.map +1 -1
  71. package/dist/commands/gate/index.js +51 -2
  72. package/dist/commands/gate/review-verdict.d.ts +22 -0
  73. package/dist/commands/gate/review-verdict.d.ts.map +1 -1
  74. package/dist/commands/gate/review-verdict.js +111 -0
  75. package/dist/commands/init/index.d.ts +6 -0
  76. package/dist/commands/init/index.d.ts.map +1 -1
  77. package/dist/commands/init/index.js +27 -4
  78. package/dist/commands/init/tools/index.d.ts +18 -1
  79. package/dist/commands/init/tools/index.d.ts.map +1 -1
  80. package/dist/commands/init/tools/index.js +84 -17
  81. package/dist/commands/init/tools/project-guidance.d.ts +19 -4
  82. package/dist/commands/init/tools/project-guidance.d.ts.map +1 -1
  83. package/dist/commands/init/tools/project-guidance.js +58 -15
  84. package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
  85. package/dist/commands/init/tools/workflows/index.js +5 -1
  86. package/dist/commands/instructions/instructions.types.d.ts +105 -4
  87. package/dist/commands/instructions/instructions.types.d.ts.map +1 -1
  88. package/dist/commands/instructions/instructions.types.js +4 -5
  89. package/dist/commands/instructions/instructions.utils.d.ts +161 -6
  90. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  91. package/dist/commands/instructions/instructions.utils.js +650 -12
  92. package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
  93. package/dist/commands/instructions/sync/sync.js +256 -18
  94. package/dist/commands/instructions/validate/validate.d.ts.map +1 -1
  95. package/dist/commands/instructions/validate/validate.js +36 -8
  96. package/dist/commands/pjm/index.d.ts.map +1 -1
  97. package/dist/commands/pjm/index.js +19 -4
  98. package/dist/commands/pjm/init.d.ts +3 -0
  99. package/dist/commands/pjm/init.d.ts.map +1 -1
  100. package/dist/commands/pjm/init.js +23 -5
  101. package/dist/commands/project/dispatch/canonical-role.d.ts +33 -0
  102. package/dist/commands/project/dispatch/canonical-role.d.ts.map +1 -0
  103. package/dist/commands/project/dispatch/canonical-role.js +114 -0
  104. package/dist/commands/project/dispatch/index.d.ts.map +1 -1
  105. package/dist/commands/project/dispatch/index.js +3 -1
  106. package/dist/commands/project/dispatch/record.d.ts.map +1 -1
  107. package/dist/commands/project/dispatch/record.js +146 -4
  108. package/dist/commands/shared/agents-md.d.ts +26 -5
  109. package/dist/commands/shared/agents-md.d.ts.map +1 -1
  110. package/dist/commands/shared/agents-md.js +182 -3
  111. package/dist/commands/sync/apply.d.ts.map +1 -1
  112. package/dist/commands/sync/apply.js +28 -21
  113. package/dist/commands/tools/guidance/index.d.ts +15 -0
  114. package/dist/commands/tools/guidance/index.d.ts.map +1 -0
  115. package/dist/commands/tools/guidance/index.js +81 -0
  116. package/dist/commands/tools/index.d.ts.map +1 -1
  117. package/dist/commands/tools/index.js +2 -0
  118. package/dist/commands/tools/shared/sync-evidence.d.ts.map +1 -1
  119. package/dist/commands/tools/shared/sync-evidence.js +32 -5
  120. package/dist/config/oat-config.d.ts +57 -3
  121. package/dist/config/oat-config.d.ts.map +1 -1
  122. package/dist/config/oat-config.js +223 -15
  123. package/dist/config/resolve.d.ts.map +1 -1
  124. package/dist/config/resolve.js +12 -2
  125. package/dist/engine/compute-plan.d.ts.map +1 -1
  126. package/dist/engine/compute-plan.js +31 -3
  127. package/dist/providers/claude/dispatch-envelope.d.ts +53 -0
  128. package/dist/providers/claude/dispatch-envelope.d.ts.map +1 -1
  129. package/dist/providers/claude/dispatch-envelope.js +324 -48
  130. package/dist/providers/claude/rule-transform.js +1 -1
  131. package/dist/providers/copilot/rule-transform.js +1 -1
  132. package/dist/providers/cursor/rule-transform.js +1 -1
  133. package/dist/providers/identity/absolute-paths.d.ts +13 -0
  134. package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
  135. package/dist/providers/identity/absolute-paths.js +27 -14
  136. package/dist/providers/identity/generic-dispatch-record.d.ts +212 -0
  137. package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -1
  138. package/dist/providers/identity/generic-dispatch-record.js +78 -19
  139. package/dist/providers/identity/oat-dispatch-record.d.ts +5 -0
  140. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
  141. package/dist/providers/identity/oat-dispatch-record.js +11 -2
  142. package/dist/providers/shared/registry.d.ts +6 -0
  143. package/dist/providers/shared/registry.d.ts.map +1 -1
  144. package/dist/providers/shared/registry.js +30 -0
  145. package/dist/rules/canonical/parse.d.ts.map +1 -1
  146. package/dist/rules/canonical/parse.js +38 -3
  147. package/package.json +2 -2
@@ -0,0 +1,114 @@
1
+ import { isAbsolute, join, resolve } from 'node:path';
2
+ import { resolveCanonicalRole, } from '../../../agents/canonical/index.js';
3
+ import { readGlobalOptions } from '../../shared/shared.utils.js';
4
+ import { dirExists, fileExists } from '../../../fs/io.js';
5
+ import { safeParseOatDispatchEvidenceEvent } from '../../../providers/identity/oat-dispatch-record.js';
6
+ import { Command } from 'commander';
7
+ import { redactDispatchMessage } from './record.js';
8
+ /** The pack that ships the canonical OAT dispatch roles. */
9
+ export const CANONICAL_ROLE_DEPENDENCY = 'workflows';
10
+ const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
11
+ const ROLE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
12
+ async function loadedSkillDir(cwd, skillDir) {
13
+ const candidate = isAbsolute(skillDir) ? skillDir : resolve(cwd, skillDir);
14
+ if (!(await dirExists(candidate)) ||
15
+ !(await fileExists(join(candidate, 'SKILL.md')))) {
16
+ throw new Error(`--skill-dir must name a loaded skill directory containing SKILL.md (got ${skillDir}); the loaded tier is derived from it.`);
17
+ }
18
+ return candidate;
19
+ }
20
+ /**
21
+ * Produce `canonical-role-resolution` evidence from the resolver itself, so a
22
+ * caller never hand-assembles redacted paths or a content digest. Read-only:
23
+ * it inspects the loaded, user, and project `.agents` roots and writes
24
+ * nothing. The emitted event is checked against the recorder's own event
25
+ * schema before it is printed, so `oat project dispatch record` accepts it
26
+ * unchanged.
27
+ */
28
+ export async function produceCanonicalRoleEvent(input) {
29
+ if (!ROLE_NAME_PATTERN.test(input.role)) {
30
+ throw new Error('--role must be a canonical role name: expected [a-z0-9] followed by [a-z0-9_-].');
31
+ }
32
+ if (!REQUEST_ID_PATTERN.test(input.requestId)) {
33
+ throw new Error('--request-id must be a stable contained identifier: expected [a-zA-Z0-9] followed by up to 127 of [a-zA-Z0-9._-].');
34
+ }
35
+ const skillDir = await loadedSkillDir(input.cwd, input.skillDir);
36
+ const event = {
37
+ kind: 'canonical-role-resolution',
38
+ requestId: input.requestId,
39
+ source: 'canonical-role-resolver',
40
+ evidence: resolveCanonicalRole({
41
+ dependency: CANONICAL_ROLE_DEPENDENCY,
42
+ canonicalRole: input.role,
43
+ skillDir,
44
+ userCanonicalRoot: join(input.home, '.agents'),
45
+ projectCanonicalRoot: join(input.repoRoot, '.agents'),
46
+ }),
47
+ };
48
+ const checked = safeParseOatDispatchEvidenceEvent(event);
49
+ if (!checked.success) {
50
+ throw new Error(`Canonical role evidence failed the dispatch-record event schema: ${checked.error.issues
51
+ .map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
52
+ .join('; ')}`);
53
+ }
54
+ return event;
55
+ }
56
+ function summaryLines(event) {
57
+ const { evidence } = event;
58
+ if (evidence.status === 'resolved') {
59
+ return [
60
+ `Canonical role ${evidence.canonicalRole} resolved from the ${evidence.tier} tier (${evidence.validation}, version ${evidence.roleVersion}).`,
61
+ ];
62
+ }
63
+ return [
64
+ `Canonical role ${evidence.canonicalRole} was not found in the loaded, user, or project tier.`,
65
+ ...evidence.recovery.map(({ command }) => `Recovery: ${command}`),
66
+ ];
67
+ }
68
+ async function runCanonicalRoleCommand(options, context, dependencies) {
69
+ let repoRoot = null;
70
+ try {
71
+ repoRoot = await dependencies.resolveProjectRoot(context.cwd);
72
+ const event = await produceCanonicalRoleEvent({
73
+ role: options.role,
74
+ requestId: options.requestId,
75
+ skillDir: options.skillDir,
76
+ cwd: context.cwd,
77
+ home: context.home,
78
+ repoRoot,
79
+ });
80
+ if (context.json) {
81
+ context.logger.json(event);
82
+ }
83
+ else {
84
+ for (const line of summaryLines(event)) {
85
+ context.logger.info(line);
86
+ }
87
+ context.logger.info(JSON.stringify(event, null, 2));
88
+ }
89
+ process.exitCode = 0;
90
+ }
91
+ catch (error) {
92
+ // The same single redaction boundary as `dispatch record`: a skill
93
+ // directory or root that reaches a message is labelled or scrubbed.
94
+ const message = redactDispatchMessage(error instanceof Error ? error.message : String(error), { repo: repoRoot, home: context.home });
95
+ if (context.json) {
96
+ context.logger.json({ status: 'error', message });
97
+ }
98
+ else {
99
+ context.logger.error(message);
100
+ }
101
+ process.exitCode = 1;
102
+ }
103
+ }
104
+ export function createCanonicalRoleCommand(dependencies) {
105
+ return new Command('canonical-role')
106
+ .description('Print canonical-role-resolution evidence for a dispatch record (read-only)')
107
+ .requiredOption('--role <name>', 'Canonical role name, e.g. oat-reviewer')
108
+ .requiredOption('--request-id <id>', 'Request ID of the dispatch record the evidence belongs to')
109
+ .requiredOption('--skill-dir <dir>', 'Directory of the loaded skill (contains SKILL.md); the loaded tier is derived from it')
110
+ .action(async (options, command) => {
111
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
112
+ await runCanonicalRoleCommand(options, context, dependencies);
113
+ });
114
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,MAAM,WAAW,kCAAkC;IACjD,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AA0ID,wBAAgB,4BAA4B,CAC1C,SAAS,GAAE,OAAO,CAAC,kCAAkC,CAAM,GAC1D,OAAO,CA2BT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,MAAM,WAAW,kCAAkC;IACjD,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AA0ID,wBAAgB,4BAA4B,CAC1C,SAAS,GAAE,OAAO,CAAC,kCAAkC,CAAM,GAC1D,OAAO,CA4BT"}
@@ -5,6 +5,7 @@ import { readGlobalOptions } from '../../shared/shared.utils.js';
5
5
  import { fileExists } from '../../../fs/io.js';
6
6
  import { resolveProjectRoot, validateRealPathWithinScope } from '../../../fs/paths.js';
7
7
  import { Command } from 'commander';
8
+ import { createCanonicalRoleCommand } from './canonical-role.js';
8
9
  import { recordProjectDispatch, redactDispatchMessage, } from './record.js';
9
10
  const DEFAULT_DEPENDENCIES = {
10
11
  buildCommandContext,
@@ -133,5 +134,6 @@ export function createProjectDispatchCommand(overrides = {}) {
133
134
  .action(async (options, command) => {
134
135
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
135
136
  await runRecordCommand(options, context, dependencies);
136
- }));
137
+ }))
138
+ .addCommand(createCanonicalRoleCommand(dependencies));
137
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/record.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAOL,KAAK,wBAAwB,EAE7B,KAAK,4BAA4B,EAElC,MAAM,yCAAyC,CAAC;AAWjD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAoCD;;;;;;;;;GASG;AAEH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,GAAE;IACL,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjB,GACL,MAAM,CAcR;AAaD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,KAAK,EAAE,wBAAwB,CAAC;IAChC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;IACF,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,GAAG,IAAI,CAAC;IACT,KAAK,EAAE,UAAU,GAAG,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC5D;;;OAGG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;CACrC;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAAC;IACvC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,4BAA4B,CAAC;IACrC,eAAe,EAAE,6BAA6B,CAAC;CAChD,CAAC;AAsEF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CA0G5E;AAgKD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,0BAA0B,CAAC;CAC3C,GAAG,OAAO,CAAC,2BAA2B,CAAC,CA6HvC"}
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/record.ts"],"names":[],"mappings":"AAuBA,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAQL,KAAK,wBAAwB,EAE7B,KAAK,4BAA4B,EAElC,MAAM,yCAAyC,CAAC;AAYjD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAoCD;;;;;;;;;GASG;AAEH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,GAAE;IACL,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjB,GACL,MAAM,CAiBR;AAeD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,KAAK,EAAE,wBAAwB,CAAC;IAChC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,CAAC;IACF,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;KAC5B,GAAG,IAAI,CAAC;IACT,KAAK,EAAE,UAAU,GAAG,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC5D;;;OAGG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;CACrC;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAAC;IACvC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,4BAA4B,CAAC;IACrC,eAAe,EAAE,6BAA6B,CAAC;CAChD,CAAC;AA8NF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CA6G5E;AAgKD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,0BAA0B,CAAC;CAC3C,GAAG,OAAO,CAAC,2BAA2B,CAAC,CA6HvC"}
@@ -1,13 +1,14 @@
1
1
  import { readFile, readdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { publishContainedJsonRevision, redactedFsError, withContainedWriterLock, } from '../../../fs/io.js';
4
- import { acceptClaudeLaunchEnvelope, buildClaudeDispatchRecord, } from '../../../providers/claude/dispatch-envelope.js';
4
+ import { acceptClaudeLaunchEnvelope, buildClaudeDispatchRecord, collectClaudeLaunchViolations, collectClaudeRecordBaseViolations, ManagedClaudeDispatchValidationError, zodIssueViolations, } from '../../../providers/claude/dispatch-envelope.js';
5
5
  import { assertJournalIdentityHasNoAbsolutePath, redactAbsolutePaths, } from '../../../providers/identity/absolute-paths.js';
6
6
  import { CLAUDE_OBSERVATION_SOURCE } from '../../../providers/identity/claude-runtime-observation.js';
7
7
  import { CODEX_OBSERVATION_SOURCE } from '../../../providers/identity/codex-runtime-observation.js';
8
- import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, } from '../../../providers/identity/generic-dispatch-record.js';
9
- import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, } from '../../../providers/identity/oat-dispatch-record.js';
8
+ import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, collectSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, redactSensitiveValues, } from '../../../providers/identity/generic-dispatch-record.js';
9
+ import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, safeParseOatDispatchEvidenceEvent, } from '../../../providers/identity/oat-dispatch-record.js';
10
10
  import { observationFromFacts, parseRuntimeObservationEnvelope, projectRuntimeObservation, providerSupportsRuntimeObservation, } from '../../../providers/identity/runtime-observation.js';
11
+ import { ZodError } from 'zod';
11
12
  const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
12
13
  const DISPATCH_LOCK_NAME = '.dispatch-lock';
13
14
  /**
@@ -48,7 +49,10 @@ function parseJournalEntry(fileName) {
48
49
  * written verbatim into the durable record by the same command.
49
50
  */
50
51
  export function redactDispatchMessage(message, roots = {}) {
51
- let redacted = message;
52
+ // Secrets first: a rejected value is echoed by some validation messages (a
53
+ // Zod enum issue repeats what it received), so a secret-shaped value must be
54
+ // scrubbed here even though the same run also reports it as sensitive.
55
+ let redacted = redactSensitiveValues(message);
52
56
  const labelled = [
53
57
  ['<project>', roots.project],
54
58
  ['<repo>', roots.repo],
@@ -71,6 +75,8 @@ const OBSERVATION_SOURCE_BY_PROVIDER = {
71
75
  claude: CLAUDE_OBSERVATION_SOURCE,
72
76
  };
73
77
  const CALLER_ASSERTED_OBSERVATION_SOURCE = 'caller-asserted';
78
+ /** A managed Claude record's provider is derived, and always this value. */
79
+ const MANAGED_CLAUDE_PROVIDER = 'claude';
74
80
  function isRecord(value) {
75
81
  return value !== null && typeof value === 'object' && !Array.isArray(value);
76
82
  }
@@ -123,6 +129,139 @@ function resolveObservationEvent(record, event) {
123
129
  }),
124
130
  };
125
131
  }
132
+ /**
133
+ * Project a caller-form `runtime-observation` event onto the shape the event
134
+ * schema checks, so the single-run pass reports its errors alongside every
135
+ * other stage instead of on a later run.
136
+ *
137
+ * The caller controls the envelope (`kind`, `requestId`, `source`, unknown
138
+ * keys) and either a finished `observation` or a `metadata` envelope, and
139
+ * both are checked here. The recording path derives an observation's
140
+ * `source`, `match`, and `comparedAxes` from the record, so those are filled
141
+ * with valid stand-ins rather than demanded from the caller; the metadata
142
+ * form's projection and request-correlation guard degrade to `not-reported`
143
+ * rather than fail, so they contribute no violation. The provider binding is
144
+ * checked directly: a managed record's provider is always `claude`.
145
+ */
146
+ function runtimeObservationCallerForm(event, violations) {
147
+ if ('metadata' in event) {
148
+ if ('observation' in event) {
149
+ violations.push({
150
+ stage: 'event',
151
+ path: '<root>',
152
+ message: 'A runtime observation event carries either observation or metadata, not both.',
153
+ });
154
+ }
155
+ try {
156
+ parseRuntimeObservationEnvelope(event.metadata);
157
+ }
158
+ catch (error) {
159
+ if (error instanceof ZodError) {
160
+ violations.push(...zodIssueViolations('event', 'metadata', error));
161
+ }
162
+ else {
163
+ violations.push({
164
+ stage: 'event',
165
+ path: 'metadata',
166
+ message: error instanceof Error ? error.message : String(error),
167
+ });
168
+ }
169
+ }
170
+ const { metadata: _metadata, observation: _observation, ...rest } = event;
171
+ return { ...rest, observation: { status: 'not-reported' } };
172
+ }
173
+ const observation = event.observation;
174
+ if (!isRecord(observation) || observation.status !== 'reported') {
175
+ return event;
176
+ }
177
+ if (observation.provider !== MANAGED_CLAUDE_PROVIDER) {
178
+ violations.push({
179
+ stage: 'event',
180
+ path: 'observation.provider',
181
+ message: 'A runtime observation must name the same provider as its dispatch record.',
182
+ });
183
+ }
184
+ return {
185
+ ...event,
186
+ observation: {
187
+ ...observation,
188
+ provider: MANAGED_CLAUDE_PROVIDER,
189
+ source: CALLER_ASSERTED_OBSERVATION_SOURCE,
190
+ match: 'not-comparable',
191
+ comparedAxes: [],
192
+ },
193
+ };
194
+ }
195
+ /**
196
+ * Event violations for a managed input: the event must be an object, carry no
197
+ * sensitive content, satisfy the evidence-event schema, and name the same
198
+ * request as `recordBase`. Raw provider `metadata` entries are exempt from the
199
+ * sensitive walk exactly as on the recording path. A `runtime-observation`
200
+ * event is validated here too: `runtimeObservationCallerForm` projects its
201
+ * caller form onto the schema shape, filling the fields the recording path
202
+ * derives (source, match, compared axes) with valid stand-ins, so its errors
203
+ * join this single-run report.
204
+ */
205
+ function collectManagedEventViolations(event, recordBase) {
206
+ if (!isRecord(event)) {
207
+ return [
208
+ {
209
+ stage: 'event',
210
+ path: '<root>',
211
+ message: 'Dispatch record event must be a JSON object.',
212
+ },
213
+ ];
214
+ }
215
+ const violations = [];
216
+ const walked = isRecord(event.metadata)
217
+ ? { ...event, metadata: { ...event.metadata, entries: null } }
218
+ : event;
219
+ violations.push(...collectSensitiveDispatchContent(walked, 'event').map((violation) => ({
220
+ stage: 'event',
221
+ path: violation.path.slice('event.'.length) || '<root>',
222
+ message: violation.message,
223
+ })));
224
+ const parsed = safeParseOatDispatchEvidenceEvent(event.kind === 'runtime-observation'
225
+ ? runtimeObservationCallerForm(event, violations)
226
+ : event);
227
+ if (!parsed.success) {
228
+ violations.push(...zodIssueViolations('event', null, parsed.error));
229
+ }
230
+ const recordRequestId = isRecord(recordBase) ? recordBase.request_id : null;
231
+ if (typeof event.requestId === 'string' &&
232
+ typeof recordRequestId === 'string' &&
233
+ event.requestId !== recordRequestId) {
234
+ violations.push({
235
+ stage: 'event',
236
+ path: 'requestId',
237
+ message: 'OAT event request ID must match the generic record.',
238
+ });
239
+ }
240
+ return violations;
241
+ }
242
+ /**
243
+ * The single-run pass for a managed Claude input. Every stage is validated
244
+ * independently and every violation is reported in one error, so a caller
245
+ * assembling the input fixes it in one round instead of one field per run.
246
+ * Checks that depend on a stage which failed to parse are skipped and named in
247
+ * the error; the recording path below still runs every check afterwards, so
248
+ * nothing this pass skips goes unchecked.
249
+ */
250
+ function assertManagedClaudeInputValid(value) {
251
+ const launch = collectClaudeLaunchViolations(value.claudeLaunch);
252
+ const base = collectClaudeRecordBaseViolations(value.recordBase, launch.resolverRole);
253
+ const violations = [
254
+ ...launch.violations,
255
+ ...base.violations,
256
+ ...collectManagedEventViolations(value.event, value.recordBase),
257
+ ];
258
+ if (violations.length > 0) {
259
+ throw new ManagedClaudeDispatchValidationError(violations, [
260
+ ...launch.skipped,
261
+ ...base.skipped,
262
+ ]);
263
+ }
264
+ }
126
265
  export function parseDispatchRecordInput(value) {
127
266
  if (!isRecord(value)) {
128
267
  throw new Error('Dispatch record input must be a JSON object.');
@@ -136,6 +275,9 @@ export function parseDispatchRecordInput(value) {
136
275
  if (!isGenericInput && !isManagedClaudeInput) {
137
276
  throw new Error('Dispatch record input accepts record and event, or claudeLaunch, event, and recordBase.');
138
277
  }
278
+ if (isManagedClaudeInput) {
279
+ assertManagedClaudeInputValid(value);
280
+ }
139
281
  if (!isRecord(value.event)) {
140
282
  throw new Error('Dispatch record event must be a JSON object.');
141
283
  }
@@ -1,13 +1,16 @@
1
- import { lstat, readFile, readlink, realpath, writeFile } from 'node:fs/promises';
1
+ import { lstat, open, readFile, readlink, realpath, writeFile } from 'node:fs/promises';
2
2
  /**
3
3
  * Plans repository AGENTS.md guidance without replacing existing paths.
4
4
  *
5
- * An absent root file may be created with one exclusive write. Existing files
6
- * and contained symlinks are read only: matching content returns no-change and
7
- * every required change is returned as a copy-pasteable manual patch.
5
+ * An absent root file may be created with one exclusive write. For an existing
6
+ * file (or its contained symlink target), matching content returns no-change,
7
+ * absent managed blocks are appended with one append-only write that never
8
+ * truncates, renames, or rewrites existing bytes, and a present-but-different
9
+ * block is returned as a zero-write, copy-pasteable manual patch.
8
10
  */
9
11
  export interface AgentsMdFileSystem {
10
12
  lstat: typeof lstat;
13
+ open: typeof open;
11
14
  readFile: typeof readFile;
12
15
  readlink: typeof readlink;
13
16
  realpath: typeof realpath;
@@ -22,6 +25,11 @@ export interface AgentsMdManualPatch {
22
25
  managedBlock: string;
23
26
  legacyBlockAction: 'preserve' | 'remove-manually';
24
27
  instructions: readonly string[];
28
+ /**
29
+ * Present when OAT refused to append absent blocks (zero bytes written);
30
+ * names the cause so callers do not claim an existing block differs.
31
+ */
32
+ appendRefusal?: string;
25
33
  }
26
34
  export interface AgentsMdBlocked {
27
35
  code: 'blocked';
@@ -30,7 +38,7 @@ export interface AgentsMdBlocked {
30
38
  action: string;
31
39
  }
32
40
  export interface UpsertSectionResult {
33
- action: 'created' | 'no-change' | 'manual-required' | 'blocked';
41
+ action: 'created' | 'appended' | 'no-change' | 'manual-required' | 'blocked';
34
42
  manualPatch?: AgentsMdManualPatch;
35
43
  blocked?: AgentsMdBlocked;
36
44
  }
@@ -38,8 +46,21 @@ export interface AgentsMdSectionInput {
38
46
  key: string;
39
47
  body: string;
40
48
  }
49
+ /**
50
+ * Renders one managed block exactly as the writer creates or appends it, so
51
+ * read-only surfaces print the same bytes the writer would produce.
52
+ */
53
+ export declare function buildAgentsMdManagedBlock(key: string, body: string): string;
41
54
  export declare function formatAgentsMdMutationFailure(error: unknown): string;
42
55
  export declare function formatAgentsMdGuidanceResult(result: UpsertSectionResult): readonly string[];
56
+ /**
57
+ * Open flags for the append-only write. A write access mode is required:
58
+ * `O_APPEND | O_NOFOLLOW` alone opens read-only and the write fails with
59
+ * `EBADF`. `O_NOFOLLOW` refuses a symlink swapped in at the final component,
60
+ * and `O_NONBLOCK` makes a FIFO swapped in fail fast (`ENXIO`) instead of
61
+ * hanging; it has no effect on regular-file writes.
62
+ */
63
+ export declare const AGENTS_MD_APPEND_FLAGS: number;
43
64
  export declare function upsertAgentsMdSections(repoRoot: string, sections: readonly AgentsMdSectionInput[], options?: Pick<AgentsMdMutationOptions, 'fileSystem'>): Promise<Record<string, UpsertSectionResult>>;
44
65
  export declare function upsertAgentsMdSection(repoRoot: string, key: string, body: string, options?: AgentsMdMutationOptions): Promise<UpsertSectionResult>;
45
66
  export declare function removeAgentsMdSection(repoRoot: string, key: string, options?: Pick<AgentsMdMutationOptions, 'fileSystem'>): Promise<boolean | 'manual-required' | 'blocked'>;
@@ -1 +1 @@
1
- {"version":3,"file":"agents-md.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/agents-md.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,EACV,MAAM,kBAAkB,CAAC;AAG1B;;;;;;GAMG;AAEH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,SAAS,EAAE,OAAO,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAgCD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,UAAU,GAAG,iBAAiB,CAAC;IAClD,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,iBAAiB,GAAG,SAAS,CAAC;IAChE,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAmTD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGpE;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,mBAAmB,GAC1B,SAAS,MAAM,EAAE,CAoBnB;AA4ID,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,OAAO,GAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAM,GACxD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAS9C;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,mBAAmB,CAAC,CAS9B;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAM,GACxD,OAAO,CAAC,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAAC,CAYlD"}
1
+ {"version":3,"file":"agents-md.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/agents-md.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,EACV,MAAM,kBAAkB,CAAC;AAG1B;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,SAAS,EAAE,OAAO,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAmCD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,UAAU,GAAG,iBAAiB,CAAC;IAClD,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,iBAAiB,GAAG,SAAS,CAAC;IAC7E,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAcD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AAySD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGpE;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,mBAAmB,GAC1B,SAAS,MAAM,EAAE,CAoBnB;AAmDD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAIb,CAAC;AA+RvB,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,OAAO,GAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAM,GACxD,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAS9C;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,mBAAmB,CAAC,CAS9B;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAM,GACxD,OAAO,CAAC,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAAC,CAYlD"}
@@ -1,7 +1,9 @@
1
- import { lstat, readFile, readlink, realpath, writeFile, } from 'node:fs/promises';
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, readFile, readlink, realpath, writeFile, } from 'node:fs/promises';
2
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
4
  const defaultFileSystem = {
4
5
  lstat,
6
+ open,
5
7
  readFile,
6
8
  readlink,
7
9
  realpath,
@@ -16,6 +18,13 @@ function sectionEnd(key) {
16
18
  function buildSection(key, body) {
17
19
  return `${sectionStart(key)}\n${body}\n${sectionEnd(key)}`;
18
20
  }
21
+ /**
22
+ * Renders one managed block exactly as the writer creates or appends it, so
23
+ * read-only surfaces print the same bytes the writer would produce.
24
+ */
25
+ export function buildAgentsMdManagedBlock(key, body) {
26
+ return buildSection(key, body);
27
+ }
19
28
  function identityOf(stat) {
20
29
  return { device: String(stat.dev), inode: String(stat.ino) };
21
30
  }
@@ -120,6 +129,7 @@ async function planAgentsMd(repoRoot, fileSystem) {
120
129
  kind: 'file',
121
130
  agentsIdentity: identityOf(agentsStat),
122
131
  targetIdentity: identityOf(agentsStat),
132
+ targetLinkCount: Number(agentsStat.nlink),
123
133
  };
124
134
  }
125
135
  if (!agentsStat.isSymbolicLink()) {
@@ -158,6 +168,7 @@ async function planAgentsMd(repoRoot, fileSystem) {
158
168
  kind: 'symlink',
159
169
  agentsIdentity: identityOf(agentsStat),
160
170
  targetIdentity: identityOf(targetStat),
171
+ targetLinkCount: Number(targetStat.nlink),
161
172
  linkText,
162
173
  };
163
174
  }
@@ -212,7 +223,7 @@ function safeReason(error) {
212
223
  if (!(error instanceof Error)) {
213
224
  return 'AGENTS.md guidance could not be planned safely.';
214
225
  }
215
- if (/^(Repository root|Repository-root AGENTS\.md|Repository or AGENTS\.md|AGENTS\.md section|AGENTS\.md managed sections|AGENTS\.md content)/.test(error.message)) {
226
+ if (/^(Repository root|Repository-root AGENTS\.md|Repository or AGENTS\.md|AGENTS\.md section|AGENTS\.md managed sections|AGENTS\.md content|AGENTS\.md append)/.test(error.message)) {
216
227
  return error.message;
217
228
  }
218
229
  return 'AGENTS.md guidance could not be planned safely.';
@@ -253,13 +264,19 @@ export function formatAgentsMdGuidanceResult(result) {
253
264
  }
254
265
  return [];
255
266
  }
256
- function createManualPatch(plan, managedBlocks, legacyKeys) {
267
+ function createManualPatch(plan, managedBlocks, legacyKeys, appendRefusal) {
257
268
  const target = targetIdentifier(plan);
258
269
  return {
259
270
  target,
260
271
  managedBlock: managedBlocks.join('\n\n'),
261
272
  legacyBlockAction: legacyKeys.length > 0 ? 'remove-manually' : 'preserve',
273
+ ...(appendRefusal ? { appendRefusal } : {}),
262
274
  instructions: [
275
+ ...(appendRefusal
276
+ ? [
277
+ `OAT did not append to ${target}: ${appendRefusal}. Nothing was written.`,
278
+ ]
279
+ : []),
263
280
  `Open ${target}.`,
264
281
  'Replace each matching OAT managed block, or append each absent block, exactly as shown.',
265
282
  ...(legacyKeys.length > 0
@@ -287,6 +304,121 @@ async function createMissingFile(plan, content, fileSystem) {
287
304
  return 'blocked';
288
305
  }
289
306
  }
307
+ /**
308
+ * Open flags for the append-only write. A write access mode is required:
309
+ * `O_APPEND | O_NOFOLLOW` alone opens read-only and the write fails with
310
+ * `EBADF`. `O_NOFOLLOW` refuses a symlink swapped in at the final component,
311
+ * and `O_NONBLOCK` makes a FIFO swapped in fail fast (`ENXIO`) instead of
312
+ * hanging; it has no effect on regular-file writes.
313
+ */
314
+ export const AGENTS_MD_APPEND_FLAGS = constants.O_WRONLY |
315
+ constants.O_APPEND |
316
+ constants.O_NOFOLLOW |
317
+ constants.O_NONBLOCK;
318
+ const HARD_LINK_REFUSAL = 'the file has more than one hard link, so an append could change another path';
319
+ function describeErrorCode(error) {
320
+ switch (errorCode(error)) {
321
+ case 'EACCES':
322
+ case 'EPERM':
323
+ return 'permission denied';
324
+ case 'EROFS':
325
+ return 'read-only file system';
326
+ case 'ENOSPC':
327
+ return 'no space left on device';
328
+ case 'EDQUOT':
329
+ return 'disk quota exceeded';
330
+ case undefined:
331
+ return 'unexpected error';
332
+ default:
333
+ return `error ${errorCode(error)}`;
334
+ }
335
+ }
336
+ const IDENTITY_CHANGED = 'Repository or AGENTS.md identity changed during planning.';
337
+ /**
338
+ * Appends absent managed blocks to an existing, already-approved target.
339
+ *
340
+ * The opened handle's device/inode must equal the planned target identity, so
341
+ * a target swapped in after planning (for example a hard link to a file
342
+ * outside the repository renamed over AGENTS.md) is refused with zero bytes
343
+ * written. A handle with more than one hard link is refused the same way, and
344
+ * a non-regular file is blocked. The payload always starts with one newline,
345
+ * so the first marker starts its own line however the file (or a concurrent
346
+ * writer) ended, and `O_APPEND` places it after every byte already there.
347
+ */
348
+ async function appendAbsentSections(plan, blocks, fileSystem) {
349
+ let handle;
350
+ try {
351
+ handle = await fileSystem.open(plan.targetPath, AGENTS_MD_APPEND_FLAGS);
352
+ }
353
+ catch (error) {
354
+ const code = errorCode(error);
355
+ // The planned path was a regular file; any of these means something else
356
+ // now sits at it (a symlink, a directory, or nothing), so it is an
357
+ // identity change, not a write refusal. EMLINK is the BSD O_NOFOLLOW
358
+ // error for a symlink.
359
+ if (code === 'ELOOP' ||
360
+ code === 'EMLINK' ||
361
+ code === 'ENOENT' ||
362
+ code === 'EISDIR' ||
363
+ code === 'ENOTDIR') {
364
+ return { kind: 'blocked', reason: IDENTITY_CHANGED };
365
+ }
366
+ if (code === 'ENXIO') {
367
+ return {
368
+ kind: 'blocked',
369
+ reason: 'AGENTS.md append target is not a regular file.',
370
+ };
371
+ }
372
+ return { kind: 'refused', cause: describeErrorCode(error) };
373
+ }
374
+ let outcome;
375
+ let offset = 0;
376
+ const payload = Buffer.from(`\n${blocks.join('\n\n')}\n`, 'utf8');
377
+ try {
378
+ const opened = await handle.stat();
379
+ if (!opened.isFile()) {
380
+ outcome = {
381
+ kind: 'blocked',
382
+ reason: 'AGENTS.md append target is not a regular file.',
383
+ };
384
+ }
385
+ else if (!hasIdentity(opened, plan.targetIdentity)) {
386
+ outcome = { kind: 'blocked', reason: IDENTITY_CHANGED };
387
+ }
388
+ else if (Number(opened.nlink) !== 1) {
389
+ outcome = { kind: 'refused', cause: HARD_LINK_REFUSAL };
390
+ }
391
+ else {
392
+ while (offset < payload.length) {
393
+ const { bytesWritten } = await handle.write(payload, offset, payload.length - offset);
394
+ if (bytesWritten <= 0) {
395
+ throw Object.assign(new Error('Append wrote no bytes.'), {
396
+ code: 'EIO',
397
+ });
398
+ }
399
+ offset += bytesWritten;
400
+ }
401
+ outcome = { kind: 'appended' };
402
+ }
403
+ }
404
+ catch (error) {
405
+ outcome =
406
+ offset === 0
407
+ ? { kind: 'refused', cause: describeErrorCode(error) }
408
+ : {
409
+ kind: 'blocked',
410
+ reason: `AGENTS.md append stopped after writing part of the managed block (${describeErrorCode(error)}); the file may now end with a partial OAT block. Remove the partial block by hand, then rerun.`,
411
+ };
412
+ }
413
+ try {
414
+ await handle.close();
415
+ }
416
+ catch {
417
+ // Every byte is already written (or the outcome is already a refusal); a
418
+ // close error does not change what the file contains.
419
+ }
420
+ return outcome;
421
+ }
290
422
  async function upsertSectionsInternal(repoRoot, sections, removeSectionKeys, fileSystem, allowReplan) {
291
423
  let plan;
292
424
  try {
@@ -332,6 +464,53 @@ async function upsertSectionsInternal(repoRoot, sections, removeSectionKeys, fil
332
464
  if (changed.length === 0 && legacy.length === 0) {
333
465
  return Object.fromEntries(desired.map(({ key }) => [key, { action: 'no-change' }]));
334
466
  }
467
+ // Appending cannot remove a legacy block, so any legacy block keeps the
468
+ // whole request on the zero-write manual patch.
469
+ if (legacy.length === 0) {
470
+ const absent = managed.filter(({ range }) => !range);
471
+ const different = managed.filter(({ block, range }) => range !== undefined &&
472
+ content.slice(range.start, range.end) !== block);
473
+ let appendRefusal;
474
+ if (absent.length > 0) {
475
+ if (plan.targetLinkCount !== 1) {
476
+ appendRefusal = HARD_LINK_REFUSAL;
477
+ }
478
+ else {
479
+ const appended = await appendAbsentSections(plan, absent.map(({ block }) => block), fileSystem);
480
+ if (appended.kind === 'blocked')
481
+ throw new Error(appended.reason);
482
+ if (appended.kind === 'refused')
483
+ appendRefusal = appended.cause;
484
+ }
485
+ }
486
+ if (appendRefusal !== undefined) {
487
+ // Zero bytes were written: every absent or different block goes on
488
+ // one manual patch that names why OAT did not append.
489
+ const refusalPatch = createManualPatch(plan, managed
490
+ .filter(({ block, range }) => !range || content.slice(range.start, range.end) !== block)
491
+ .map(({ block }) => block), [], appendRefusal);
492
+ return Object.fromEntries(managed.map(({ key, range }) => [
493
+ key,
494
+ range && !different.some((entry) => entry.key === key)
495
+ ? { action: 'no-change' }
496
+ : { action: 'manual-required', manualPatch: refusalPatch },
497
+ ]));
498
+ }
499
+ const differentPatch = different.length > 0
500
+ ? createManualPatch(plan, different.map(({ block }) => block), [])
501
+ : undefined;
502
+ return Object.fromEntries(managed.map(({ key, range }) => {
503
+ if (!range)
504
+ return [key, { action: 'appended' }];
505
+ if (differentPatch && different.some((entry) => entry.key === key)) {
506
+ return [
507
+ key,
508
+ { action: 'manual-required', manualPatch: differentPatch },
509
+ ];
510
+ }
511
+ return [key, { action: 'no-change' }];
512
+ }));
513
+ }
335
514
  const manualPatch = createManualPatch(plan, changed.length > 0
336
515
  ? changed.map(({ block }) => block)
337
516
  : desired.map(({ block }) => block), legacy.map(({ key }) => key));
@@ -1 +1 @@
1
- {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoB3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EAExB,MAAM,cAAc,CAAC;AA6XtB,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,aAAa,EAAE,EAC3B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,IAAI,CAAC,CAyMf"}
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAoB3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EAExB,MAAM,cAAc,CAAC;AAwYtB,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,aAAa,EAAE,EAC3B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Mf"}