greprag 5.65.1 → 5.65.3

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 (63) hide show
  1. package/dist/codex-chip-hooks.js +2 -2
  2. package/dist/codex-chip-hooks.js.map +1 -1
  3. package/dist/commands/agent-doctrine-file.d.ts +27 -9
  4. package/dist/commands/agent-doctrine-file.js +74 -47
  5. package/dist/commands/agent-doctrine-file.js.map +1 -1
  6. package/dist/commands/agents.d.ts +2 -2
  7. package/dist/commands/agents.js +167 -48
  8. package/dist/commands/agents.js.map +1 -1
  9. package/dist/commands/codex-chip/command.js +4 -2
  10. package/dist/commands/codex-chip/command.js.map +1 -1
  11. package/dist/commands/codex-chip/goals.d.ts +3 -3
  12. package/dist/commands/codex-chip/goals.js +3 -3
  13. package/dist/commands/codex-chip/goals.js.map +1 -1
  14. package/dist/commands/codex-chip/help.d.ts +1 -1
  15. package/dist/commands/codex-chip/help.js +4 -3
  16. package/dist/commands/codex-chip/help.js.map +1 -1
  17. package/dist/commands/codex-chip/model.d.ts +6 -2
  18. package/dist/commands/codex-chip/model.js +12 -2
  19. package/dist/commands/codex-chip/model.js.map +1 -1
  20. package/dist/commands/codex-chip/native.js +2 -2
  21. package/dist/commands/codex-chip/native.js.map +1 -1
  22. package/dist/commands/codex-chip/prompt.js +8 -5
  23. package/dist/commands/codex-chip/prompt.js.map +1 -1
  24. package/dist/commands/codex-chip/selection.d.ts +5 -4
  25. package/dist/commands/codex-chip/selection.js +28 -9
  26. package/dist/commands/codex-chip/selection.js.map +1 -1
  27. package/dist/commands/codex-model-policy.d.ts +1 -1
  28. package/dist/commands/codex-model-policy.js +17 -6
  29. package/dist/commands/codex-model-policy.js.map +1 -1
  30. package/dist/commands/codex.js +1 -1
  31. package/dist/commands/init.js +19 -2
  32. package/dist/commands/init.js.map +1 -1
  33. package/dist/commands/load-primer-reminder.js +1 -1
  34. package/dist/commands/load-primer-reminder.js.map +1 -1
  35. package/dist/commands/mechanic-spawn.js +2 -3
  36. package/dist/commands/mechanic-spawn.js.map +1 -1
  37. package/dist/commands/opencode-chip-mission.d.ts +4 -4
  38. package/dist/commands/opencode-chip-mission.js.map +1 -1
  39. package/dist/commands/opencode-chip-store.d.ts +2 -2
  40. package/dist/commands/opencode-chip.js +3 -2
  41. package/dist/commands/opencode-chip.js.map +1 -1
  42. package/dist/commands/parity.js +1 -1
  43. package/dist/commands/skill-mirror-reminder.d.ts +3 -4
  44. package/dist/commands/skill-mirror-reminder.js +13 -8
  45. package/dist/commands/skill-mirror-reminder.js.map +1 -1
  46. package/dist/commands/skill.js +28 -0
  47. package/dist/commands/skill.js.map +1 -1
  48. package/dist/index.js +7 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/native-skill-mirror.d.ts +30 -0
  51. package/dist/native-skill-mirror.js +230 -0
  52. package/dist/native-skill-mirror.js.map +1 -0
  53. package/dist/opencode-plugin.bundle.js +9 -4
  54. package/dist/skill-mirror-client.d.ts +2 -5
  55. package/dist/skill-mirror-client.js +18 -6
  56. package/dist/skill-mirror-client.js.map +1 -1
  57. package/dist/skill-mirror-files.d.ts +6 -0
  58. package/dist/skill-mirror-files.js +51 -0
  59. package/dist/skill-mirror-files.js.map +1 -0
  60. package/package.json +1 -1
  61. package/skill/greprag/docs/codex-chip.md +4 -6
  62. package/skill/templates/chip-leader.md +37 -15
  63. package/skill/templates/codex-chip-spawn.md +40 -28
@@ -66,8 +66,8 @@ function context(input) {
66
66
  // adr: adr/codex-landing-doctrine.md — default parent-owned landing + the one FIX exception.
67
67
  'Landing: the parent owns merge, archive, and cleanup — do not merge, push, deploy, clean up, or edit the parent checkout. ONE exception: a mission whose first line begins `FIX:` (emitted by `greprag fix spawn`) carries its own Phase 3 landing contract — after peer conferral it merges its branch to main itself; cleanup stays native (`Clearance: safe to archive`), and push/release stays operator-gated.',
68
68
  ];
69
- if (chip.codex.role === 'mechanic') {
70
- lines.push('Harness: Codex. Role: visible native MECHANIC chip with ordinary writable authority, not an internal subagent.', 'Scope: repair GrepRAG-owned harness surfaces in this checkout: load entries, skill templates, docs, hook context, CLI messages, and tests. Codex owns native task creation, worktrees, permissions, Git, and task lifecycle.', 'Own diagnosis, implementation, tests, commit, and parent report. Inspect Codex behavior only far enough to correct GrepRAG guidance or report a precise external blocker.', 'Mechanic peer handoff: if a Mechanic peer offers to coordinate, integrate, rework, or take over a harness repair, hand off branch/commit, dirty files, checks, blockers, and caveats. Do not defend patch ownership; peers may take ownership within their assigned GrepRAG harness scope.', 'Visible delegation route: load `codex-chip-spawn` and create the child with `codex_app__create_thread` using the current project worktree target; load `chip-leader` for multi-chip seams. Never use `multi_agent_v1__spawn_agent` for a visible chip.');
69
+ if ((0, model_1.chipMissionKind)(chip) === 'fix') {
70
+ lines.push('Harness: Codex. Mission: visible native FIX chip with ordinary writable authority, not an internal subagent.', 'Scope: repair GrepRAG-owned harness surfaces in this checkout: load entries, skill templates, docs, hook context, CLI messages, and tests. Codex owns native task creation, worktrees, permissions, Git, and task lifecycle.', 'Own diagnosis, implementation, tests, commit, and parent report. Inspect Codex behavior only far enough to correct GrepRAG guidance or report a precise external blocker.', 'FIX peer handoff: if a peer offers to coordinate, integrate, rework, or take over a harness repair, hand off branch/commit, dirty files, checks, blockers, and caveats. Do not defend patch ownership; peers may take ownership within their assigned GrepRAG harness scope.', 'Visible delegation route: load `codex-chip-spawn` and create the child with `codex_app__create_thread` using the current project worktree target; load `chip-leader` for multi-chip seams. Never use `multi_agent_v1__spawn_agent` for a visible chip.');
71
71
  }
72
72
  return output(input.hook_event_name || 'SessionStart', lines.join('\n'));
73
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"codex-chip-hooks.js","sourceRoot":"","sources":["../src/codex-chip-hooks.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,sDAMC;AAzFD,2CAA6B;AAE7B,uDAAoE;AACpE,uDAA2E;AAC3E,yDAAgF;AAkBhF,SAAS,WAAW,CAAC,KAAoB;IACvC,OAAO,IAAA,6BAAqB,EAAC,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,MAAM,CAAC,KAAa,EAAE,iBAAyB,EAAE,MAAe;IACvE,OAAO;QACL,kBAAkB,EAAE;YAClB,aAAa,EAAE,KAAK;YACpB,iBAAiB;YACjB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,MAAM,EAAE,wBAAwB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,KAAoB;IACnC,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,SAAS,CAAC;IAC/C,MAAM,KAAK,GAAG;QACZ,uBAAuB,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;QAC7C,SAAS,IAAA,iBAAS,EAAC,IAAI,CAAC,EAAE;QAC1B,eAAe,IAAI,CAAC,IAAI,EAAE;QAC1B,aAAa,IAAA,yBAAiB,EAAC,IAAI,CAAC,EAAE;QACtC,WAAW,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;QACjC,mBAAmB,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,cAAc,EAAE;QAC5D,uJAAuJ,GAAG,sBAAsB,IAAI,CAAC,EAAE,GAAG;QAC1L,6FAA6F;QAC7F,qZAAqZ;KACtZ,CAAC;IACF,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CACR,gHAAgH,EAChH,8NAA8N,EAC9N,2KAA2K,EAC3K,4RAA4R,EAC5R,wPAAwP,CACzP,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,cAAc,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAoB;IAC9C,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAClE,MAAM,IAAI,GAAG,IAAA,yCAAgC,EAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACtG,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACtC,CAAC;AAED;iEACiE;AACjE,SAAS,KAAK,CAAC,KAAoB;IACjC,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACzE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAA,yBAAiB,EAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACxE,IAAI,SAAS,KAAK,WAAW,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC1E,OAAO,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,YAAY,EAAE,EAAE,EACrD,kFAAkF,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,qBAAqB,CAAC,KAAoB;IACxD,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,IAAI,EAAE,CAAC;IAC1C,IAAI,KAAK,KAAK,kBAAkB;QAAE,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACnE,IAAI,KAAK,KAAK,YAAY;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC;IAChD,IAAI,KAAK,KAAK,cAAc,IAAI,KAAK,KAAK,eAAe,IAAI,KAAK,KAAK,cAAc;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7G,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"codex-chip-hooks.js","sourceRoot":"","sources":["../src/codex-chip-hooks.ts"],"names":[],"mappings":"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"}
@@ -1,30 +1,48 @@
1
- /** Pure managed-follower block helpers for canonical agent doctrine. */
1
+ /** Pure full-file follower helpers for canonical agent doctrine.
2
+ *
3
+ * Follower files must stay byte-faithful instruction files. Ownership state
4
+ * lives outside the markdown in the GrepRAG parity manifest.
5
+ */
2
6
  export interface DoctrineFollowerRender {
3
7
  scopeKey: string;
4
8
  harness: 'claude-code' | 'codex' | 'opencode';
5
9
  revision: number;
6
10
  body: string;
7
11
  }
12
+ export interface DoctrineFollowerBaseline {
13
+ syncedRevision?: number | null;
14
+ syncedHash?: string | null;
15
+ syncedAt?: string | null;
16
+ dirtyAt?: string | null;
17
+ }
8
18
  export type DoctrineFollowerOutcome = {
9
19
  action: 'current';
10
20
  content: string;
11
21
  renderHash: string;
22
+ shouldReportSync: boolean;
12
23
  } | {
13
- action: 'seed' | 'update' | 'adopt';
24
+ action: 'seed' | 'update' | 'force';
14
25
  content: string;
15
26
  renderHash: string;
27
+ shouldReportSync: true;
16
28
  } | {
17
- action: 'diverged' | 'unmanaged';
29
+ action: 'local-change' | 'conflict' | 'unmanaged';
18
30
  content: string;
19
31
  reason: string;
20
32
  renderHash: string;
33
+ shouldReportSync: false;
21
34
  };
22
35
  export declare function doctrineRenderHash(value: string): string;
23
- export declare function renderDoctrineFollowerBlock(render: DoctrineFollowerRender): string;
24
- /** Patch a generated follower without clobbering unadopted or hand-edited text.
25
- * A prior generated block is replaceable only while its actual body still
26
- * matches the hash declared in its own header. */
27
- export declare function patchDoctrineFollower(source: string, render: DoctrineFollowerRender, options?: {
36
+ /** Return only the canonical body from an old marker-managed follower. */
37
+ export declare function extractDoctrineFollowerBody(source: string): string | null;
38
+ export declare function renderDoctrineFollower(render: DoctrineFollowerRender): string;
39
+ /** Plan a full-file follower write.
40
+ *
41
+ * No marker comments are generated. A write is automatic only when the file is
42
+ * empty/missing, already exactly current, or still equals its last verified
43
+ * baseline. If both canon and disk changed since that baseline, the caller gets
44
+ * `conflict` and must not write.
45
+ */
46
+ export declare function planDoctrineFollowerWrite(source: string, render: DoctrineFollowerRender, baseline: DoctrineFollowerBaseline | null | undefined, options?: {
28
47
  force?: boolean;
29
- adoptWholeFile?: boolean;
30
48
  }): DoctrineFollowerOutcome;
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
- /** Pure managed-follower block helpers for canonical agent doctrine. */
2
+ /** Pure full-file follower helpers for canonical agent doctrine.
3
+ *
4
+ * Follower files must stay byte-faithful instruction files. Ownership state
5
+ * lives outside the markdown in the GrepRAG parity manifest.
6
+ */
3
7
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
8
  if (k2 === undefined) k2 = k;
5
9
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -35,70 +39,93 @@ var __importStar = (this && this.__importStar) || (function () {
35
39
  })();
36
40
  Object.defineProperty(exports, "__esModule", { value: true });
37
41
  exports.doctrineRenderHash = doctrineRenderHash;
38
- exports.renderDoctrineFollowerBlock = renderDoctrineFollowerBlock;
39
- exports.patchDoctrineFollower = patchDoctrineFollower;
42
+ exports.extractDoctrineFollowerBody = extractDoctrineFollowerBody;
43
+ exports.renderDoctrineFollower = renderDoctrineFollower;
44
+ exports.planDoctrineFollowerWrite = planDoctrineFollowerWrite;
40
45
  const crypto = __importStar(require("node:crypto"));
41
- function normalizedBody(value) {
42
- return value.replace(/\r\n/g, '\n').trim();
43
- }
44
46
  function doctrineRenderHash(value) {
45
- return crypto.createHash('sha256').update(normalizedBody(value)).digest('hex');
47
+ return crypto.createHash('sha256').update(value).digest('hex');
48
+ }
49
+ const LEGACY_BLOCK_RE = /<!--\s*greprag:agents:start\s+scope=\S+\s+harness=\S+\s+revision=(\d+)\s+hash=([a-f0-9]{64})(?:\s+chars=(\d+))?\s*-->\r?\n([\s\S]*?)<!--\s*greprag:agents:end\s*-->/i;
50
+ /** Return only the canonical body from an old marker-managed follower. */
51
+ function extractDoctrineFollowerBody(source) {
52
+ const match = source.match(LEGACY_BLOCK_RE);
53
+ if (!match)
54
+ return null;
55
+ const chars = match[3] === undefined ? null : Number(match[3]);
56
+ const captured = match[4];
57
+ return chars === null ? captured.replace(/\r?\n$/, '') : captured.slice(0, chars);
58
+ }
59
+ function renderDoctrineFollower(render) {
60
+ return render.body;
46
61
  }
47
- function renderDoctrineFollowerBlock(render) {
48
- const body = normalizedBody(render.body);
49
- const hash = doctrineRenderHash(body);
50
- return `<!-- greprag:agents:start scope=${render.scopeKey} harness=${render.harness} revision=${render.revision} hash=${hash} -->\n`
51
- + `${body}\n<!-- greprag:agents:end -->`;
62
+ function hasSyncedBaseline(baseline) {
63
+ return !!baseline?.syncedHash && /^[a-f0-9]{64}$/i.test(baseline.syncedHash);
52
64
  }
53
- function findBlock(source) {
54
- const pattern = /<!--\s*greprag:agents:start\s+scope=\S+\s+harness=\S+\s+revision=(\d+)\s+hash=([a-f0-9]{64})\s*-->\r?\n?([\s\S]*?)\r?\n?<!--\s*greprag:agents:end\s*-->/i;
55
- const match = source.match(pattern);
56
- return match ? {
57
- whole: match[0],
58
- revision: Number(match[1]),
59
- declaredHash: match[2].toLowerCase(),
60
- inner: match[3],
61
- } : null;
65
+ function canonChangedSinceBaseline(baseline, render, renderHash) {
66
+ if (!hasSyncedBaseline(baseline))
67
+ return false;
68
+ return baseline.syncedRevision !== render.revision || baseline.syncedHash !== renderHash;
62
69
  }
63
- /** Patch a generated follower without clobbering unadopted or hand-edited text.
64
- * A prior generated block is replaceable only while its actual body still
65
- * matches the hash declared in its own header. */
66
- function patchDoctrineFollower(source, render, options = {}) {
67
- const block = renderDoctrineFollowerBlock(render);
68
- const renderHash = doctrineRenderHash(render.body);
69
- const existing = findBlock(source);
70
- if (!existing) {
71
- if (!source.trim()) {
72
- return { action: 'seed', content: block + '\n', renderHash };
73
- }
74
- if (options.force || options.adoptWholeFile) {
75
- return { action: 'adopt', content: block + '\n', renderHash };
76
- }
70
+ /** Plan a full-file follower write.
71
+ *
72
+ * No marker comments are generated. A write is automatic only when the file is
73
+ * empty/missing, already exactly current, or still equals its last verified
74
+ * baseline. If both canon and disk changed since that baseline, the caller gets
75
+ * `conflict` and must not write.
76
+ */
77
+ function planDoctrineFollowerWrite(source, render, baseline, options = {}) {
78
+ const body = renderDoctrineFollower(render);
79
+ const renderHash = doctrineRenderHash(body);
80
+ const sourceHash = doctrineRenderHash(source);
81
+ const baselineHash = baseline?.syncedHash?.toLowerCase() || null;
82
+ const synced = hasSyncedBaseline(baseline);
83
+ if (sourceHash === renderHash) {
77
84
  return {
78
- action: 'unmanaged',
85
+ action: 'current',
79
86
  content: source,
80
87
  renderHash,
81
- reason: 'file has unmanaged content; explicitly adopt it or use --force',
88
+ shouldReportSync: baseline?.syncedRevision !== render.revision || baselineHash !== renderHash,
82
89
  };
83
90
  }
84
- const actualHash = doctrineRenderHash(existing.inner);
85
- if (!options.force && actualHash !== existing.declaredHash) {
91
+ if (options.force) {
92
+ return { action: 'force', content: body, renderHash, shouldReportSync: true };
93
+ }
94
+ if (!source.trim()) {
95
+ return { action: 'seed', content: body, renderHash, shouldReportSync: true };
96
+ }
97
+ if (!synced) {
86
98
  return {
87
- action: 'diverged',
99
+ action: 'unmanaged',
88
100
  content: source,
89
101
  renderHash,
90
- reason: 'managed follower was edited after its last canonical render',
102
+ shouldReportSync: false,
103
+ reason: 'file has no external sync baseline; enroll only when bytes already match canon or use --force',
91
104
  };
92
105
  }
93
- if (existing.revision === render.revision
94
- && actualHash === renderHash
95
- && existing.declaredHash === renderHash) {
96
- return { action: 'current', content: source, renderHash };
106
+ const diskChanged = sourceHash !== baselineHash;
107
+ const canonChanged = canonChangedSinceBaseline(baseline, render, renderHash);
108
+ const dirty = !!baseline?.dirtyAt && (!baseline.syncedAt || baseline.dirtyAt > baseline.syncedAt);
109
+ if (!diskChanged && canonChanged) {
110
+ return { action: 'update', content: body, renderHash, shouldReportSync: true };
111
+ }
112
+ if (diskChanged && canonChanged) {
113
+ return {
114
+ action: 'conflict',
115
+ content: source,
116
+ renderHash,
117
+ shouldReportSync: false,
118
+ reason: 'both canon and follower changed since the last verified sync; pull or reconcile explicitly',
119
+ };
97
120
  }
98
121
  return {
99
- action: 'update',
100
- content: source.replace(existing.whole, block),
122
+ action: 'local-change',
123
+ content: source,
101
124
  renderHash,
125
+ shouldReportSync: false,
126
+ reason: dirty
127
+ ? 'follower has a worthy direct edit after sync; pull it to canon or revert it before push'
128
+ : 'follower changed since the last verified sync; pull it to canon or revert it before push',
102
129
  };
103
130
  }
104
131
  //# sourceMappingURL=agent-doctrine-file.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-doctrine-file.js","sourceRoot":"","sources":["../../src/commands/agent-doctrine-file.ts"],"names":[],"mappings":";AAAA,wEAAwE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBxE,gDAEC;AAED,kEAKC;AAuBD,sDA0CC;AA5FD,oDAAsC;AActC,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAAa;IAC9C,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjF,CAAC;AAED,SAAgB,2BAA2B,CAAC,MAA8B;IACxE,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,mCAAmC,MAAM,CAAC,QAAQ,YAAY,MAAM,CAAC,OAAO,aAAa,MAAM,CAAC,QAAQ,SAAS,IAAI,QAAQ;UAChI,GAAG,IAAI,+BAA+B,CAAC;AAC7C,CAAC;AASD,SAAS,SAAS,CAAC,MAAc;IAC/B,MAAM,OAAO,GAAG,0JAA0J,CAAC;IAC3K,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,OAAO,KAAK,CAAC,CAAC,CAAC;QACb,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1B,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QACpC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;KAChB,CAAC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;kDAEkD;AAClD,SAAgB,qBAAqB,CACnC,MAAc,EACd,MAA8B,EAC9B,UAAyD,EAAE;IAE3D,MAAM,KAAK,GAAG,2BAA2B,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACnB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC;QAC/D,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC5C,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC;QAChE,CAAC;QACD,OAAO;YACL,MAAM,EAAE,WAAW;YACnB,OAAO,EAAE,MAAM;YACf,UAAU;YACV,MAAM,EAAE,gEAAgE;SACzE,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,UAAU,KAAK,QAAQ,CAAC,YAAY,EAAE,CAAC;QAC3D,OAAO;YACL,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,MAAM;YACf,UAAU;YACV,MAAM,EAAE,6DAA6D;SACtE,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;WAClC,UAAU,KAAK,UAAU;WACzB,QAAQ,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QAC5C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QAC9C,UAAU;KACX,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"agent-doctrine-file.js","sourceRoot":"","sources":["../../src/commands/agent-doctrine-file.ts"],"names":[],"mappings":"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"}
@@ -8,8 +8,8 @@ export interface AgentDoctrineBootstrapResult {
8
8
  blockedFollowers: number;
9
9
  }
10
10
  /** Init-time seed for the active harness. No canon is a clean no-op; only a
11
- * missing/empty follower is written. Every existing file, including a stale
12
- * managed follower, requires an explicit `agents sync`. */
11
+ * missing/empty follower is written. Every existing nonmatching file requires
12
+ * an explicit `agents push` or `agents pull`. */
13
13
  export declare function bootstrapAgentDoctrine(apiKey: string, harness: Harness): Promise<AgentDoctrineBootstrapResult>;
14
14
  export declare function runAgents(args: string[]): Promise<void>;
15
15
  export {};
@@ -46,16 +46,21 @@ const agent_doctrine_file_1 = require("./agent-doctrine-file");
46
46
  const HELP = `greprag agents — canonical CLAUDE.md / AGENTS.md doctrine
47
47
 
48
48
  USAGE
49
- greprag agents adopt <file> [--scope global|repo] [--harness <name>] [--raw] [--no-sync]
50
- greprag agents optimize [--from <file>] [--scope global|repo] [--harness <name>] [--no-sync]
49
+ greprag agents pull <file> [--scope global|repo] [--harness <name>] [--push]
50
+ greprag agents push [--scope global|repo|all] [--check] [--force]
51
+ greprag agents adopt <file> [--scope global|repo] [--harness <name>] [--no-sync]
52
+ greprag agents revise <file> [--scope global|repo] [--harness <name>] [--no-sync]
53
+ greprag agents overlay set <harness> <file> [--scope global|repo] [--no-sync]
54
+ greprag agents overlay derive <harness> <shared-file> <rendered-file> [--scope global|repo] [--no-sync]
55
+ greprag agents overlay clear <harness> [--scope global|repo] [--no-sync]
51
56
  greprag agents sync [--scope global|repo|all] [--check] [--force]
52
57
  greprag agents status [--scope global|repo|all]
53
58
 
54
59
  MODEL
55
- Adoption is bidirectional once. After adoption, load://agents is canonical
56
- and instruction files are generated followers. Direct follower edits become
57
- drift candidates; use optimize --from, inspect with load agents, then sync.
58
- --no-sync advances canon without writing followers; use it as the preview gate.`;
60
+ Adopt and revise copy source bytes exactly; no model rewrites doctrine.
61
+ Harness translations are raw additive suffixes. Pull writes disk to canon.
62
+ Push writes canon to disk only when the external sync baseline is clean.
63
+ Status/check never writes.`;
59
64
  function config(apiKeyOverride) {
60
65
  return {
61
66
  apiUrl: process.env.GREPRAG_API_URL || 'https://api.greprag.com',
@@ -133,19 +138,24 @@ function parityUnitName(scope, projectId, directory) {
133
138
  : crypto.createHash('sha256').update(directory).digest('hex').slice(0, 8);
134
139
  return `agent-doctrine-${projectId.slice(0, 8)}-${suffix}`;
135
140
  }
136
- function renderSection(title, value) {
137
- return value.trim() ? `## ${title}\n\n${value.trim()}` : '';
141
+ function uniqueNonempty(values) {
142
+ const seen = new Set();
143
+ const out = [];
144
+ for (const value of values) {
145
+ if (!value)
146
+ continue;
147
+ if (seen.has(value))
148
+ continue;
149
+ seen.add(value);
150
+ out.push(value);
151
+ }
152
+ return out;
138
153
  }
139
154
  function renderForTarget(doc, target) {
140
- const parts = [doc.shared.trim()];
141
155
  if (target.combinedAgents) {
142
- parts.push(renderSection('Codex Overlay', doc.overlays.codex));
143
- parts.push(renderSection('OpenCode Overlay', doc.overlays.opencode));
156
+ return doc.shared + uniqueNonempty([doc.overlays.codex, doc.overlays.opencode]).join('');
144
157
  }
145
- else {
146
- parts.push(renderSection(`${target.harness} Overlay`, doc.overlays[target.harness]));
147
- }
148
- return parts.filter(Boolean).join('\n\n').trim() + '\n';
158
+ return doc.shared + (doc.overlays[target.harness] || '');
149
159
  }
150
160
  async function apiJson(pathname, init, apiKeyOverride) {
151
161
  const cfg = config(apiKeyOverride);
@@ -192,6 +202,27 @@ async function fetchDocument(scope, projectId, directory, apiKey) {
192
202
  throw error;
193
203
  }
194
204
  }
205
+ async function fetchFollowerBaselines(unitName, apiKey) {
206
+ try {
207
+ const body = await apiJson('/v1/parity/list', undefined, apiKey);
208
+ const unit = body.units.find((candidate) => candidate.name === unitName);
209
+ const baselines = new Map();
210
+ for (const follower of unit?.followers || []) {
211
+ if (!follower.path)
212
+ continue;
213
+ baselines.set(`${follower.harness}\0${follower.path}`, {
214
+ syncedRevision: follower.syncedRevision,
215
+ syncedHash: follower.syncedHash,
216
+ syncedAt: follower.syncedAt,
217
+ dirtyAt: follower.dirtyAt,
218
+ });
219
+ }
220
+ return baselines;
221
+ }
222
+ catch {
223
+ return new Map();
224
+ }
225
+ }
195
226
  function inferHarness(file, explicit) {
196
227
  if (explicit === 'claude-code' || explicit === 'codex' || explicit === 'opencode')
197
228
  return explicit;
@@ -230,22 +261,22 @@ async function syncOne(params) {
230
261
  if (!doc) {
231
262
  if (!params.quiet)
232
263
  console.log(`${params.scope}: no canonical doctrine`);
233
- return { canonical: false, current: 0, changed: 0, blocked: 0, drift: 0 };
264
+ return { canonical: false, current: 0, changed: 0, enrolled: 0, blocked: 0, drift: 0 };
234
265
  }
235
266
  const unitName = parityUnitName(params.scope, ctx.projectId, ctx.directory);
236
- const stats = { canonical: true, current: 0, changed: 0, blocked: 0, drift: 0 };
267
+ const baselines = await fetchFollowerBaselines(unitName, params.apiKey);
268
+ const stats = { canonical: true, current: 0, changed: 0, enrolled: 0, blocked: 0, drift: 0 };
237
269
  for (const target of ctx.targets.filter(t => targetMatchesHarness(t, params.harness))) {
238
270
  const source = fs.existsSync(target.actualPath)
239
271
  ? fs.readFileSync(target.actualPath, 'utf8')
240
272
  : '';
241
- const adopting = !!params.adoptSourcePath
242
- && path.resolve(params.adoptSourcePath).toLowerCase() === path.resolve(target.actualPath).toLowerCase();
243
- const outcome = (0, agent_doctrine_file_1.patchDoctrineFollower)(source, {
273
+ const rendered = renderForTarget(doc, target);
274
+ const outcome = (0, agent_doctrine_file_1.planDoctrineFollowerWrite)(source, {
244
275
  scopeKey: doc.scopeKey,
245
276
  harness: target.harness,
246
277
  revision: doc.revision,
247
- body: renderForTarget(doc, target),
248
- }, { force: params.force, adoptWholeFile: adopting });
278
+ body: rendered,
279
+ }, baselines.get(`${target.harness}\0${target.manifestPath}`), { force: params.force });
249
280
  const current = outcome.action === 'current';
250
281
  if (current)
251
282
  stats.current++;
@@ -256,12 +287,20 @@ async function syncOne(params) {
256
287
  console.log(` ${current ? 'fresh' : outcome.action.padEnd(9)} ${target.actualPath}${detail}`);
257
288
  }
258
289
  const seedOnlyBlocked = params.seedOnly && !current && outcome.action !== 'seed';
259
- if (outcome.action === 'diverged' || outcome.action === 'unmanaged' || seedOnlyBlocked) {
290
+ if (outcome.action === 'conflict' || outcome.action === 'local-change'
291
+ || outcome.action === 'unmanaged' || seedOnlyBlocked) {
260
292
  stats.blocked++;
261
293
  continue;
262
294
  }
263
- if (params.check || current)
295
+ if (params.check)
264
296
  continue;
297
+ if (current) {
298
+ if (outcome.shouldReportSync) {
299
+ await reportSync(unitName, target, doc.revision, outcome.renderHash, params.apiKey);
300
+ stats.enrolled++;
301
+ }
302
+ continue;
303
+ }
265
304
  fs.mkdirSync(path.dirname(target.actualPath), { recursive: true });
266
305
  fs.writeFileSync(target.actualPath, outcome.content, 'utf8');
267
306
  await reportSync(unitName, target, doc.revision, outcome.renderHash, params.apiKey);
@@ -269,31 +308,32 @@ async function syncOne(params) {
269
308
  }
270
309
  return stats;
271
310
  }
272
- async function writeCommand(args, adopt) {
273
- const positional = adopt && args[0] && !args[0].startsWith('-') ? args[0] : undefined;
274
- const from = flag(args, '--from') || positional;
275
- if (adopt && !from)
276
- throw new Error('adopt requires a source file');
277
- const sourcePath = from ? path.resolve(from) : undefined;
278
- if (sourcePath && !fs.existsSync(sourcePath))
311
+ async function exactSourceCommand(args, mode) {
312
+ const from = args[0] && !args[0].startsWith('-') ? args[0] : undefined;
313
+ if (!from)
314
+ throw new Error(`${mode} requires a source file`);
315
+ const sourcePath = path.resolve(from);
316
+ if (!fs.existsSync(sourcePath))
279
317
  throw new Error(`source file not found: ${sourcePath}`);
280
318
  const scope = inferScope(sourcePath, flag(args, '--scope'));
281
- let harness = inferHarness(sourcePath, flag(args, '--harness'));
319
+ const harness = inferHarness(sourcePath, flag(args, '--harness'));
282
320
  const ctx = context(scope, args);
283
- const current = adopt ? null : await fetchDocument(scope, ctx.projectId, ctx.directory);
284
- if (!sourcePath && current && !flag(args, '--harness'))
285
- harness = current.sourceHarness;
286
- const body = await apiJson(`/v1/agents/${adopt ? 'adopt' : 'optimize'}`, {
321
+ const current = mode === 'adopt' ? null : await fetchDocument(scope, ctx.projectId, ctx.directory);
322
+ const endpointMode = mode === 'pull'
323
+ ? current ? 'revise' : 'adopt'
324
+ : mode;
325
+ const fileContent = fs.readFileSync(sourcePath, 'utf8');
326
+ const content = (0, agent_doctrine_file_1.extractDoctrineFollowerBody)(fileContent) ?? fileContent;
327
+ const body = await apiJson(`/v1/agents/${endpointMode}`, {
287
328
  method: 'POST',
288
329
  body: JSON.stringify({
289
330
  scope,
290
331
  projectId: ctx.projectId,
291
332
  directory: ctx.directory,
292
333
  sourceHarness: harness,
293
- sourcePath: sourcePath || null,
294
- ...(sourcePath ? { content: fs.readFileSync(sourcePath, 'utf8') } : {}),
295
- optimize: !args.includes('--raw'),
296
- expectedRevision: current?.revision ?? null,
334
+ sourcePath,
335
+ content,
336
+ expectedRevision: endpointMode === 'revise' ? current?.revision ?? null : null,
297
337
  followers: ctx.targets.map((target) => ({
298
338
  harness: target.harness,
299
339
  path: target.manifestPath,
@@ -301,17 +341,87 @@ async function writeCommand(args, adopt) {
301
341
  })),
302
342
  }),
303
343
  });
304
- console.log(`${adopt ? 'Adopted' : 'Optimized'} ${body.document.scopeKey} revision ${body.document.revision}.`);
305
- if (!args.includes('--no-sync')) {
344
+ const verb = mode === 'pull' ? 'Pulled' : mode === 'adopt' ? 'Adopted' : 'Revised';
345
+ console.log(`${verb} ${body.document.scopeKey} revision ${body.document.revision}.`);
346
+ const shouldPush = mode === 'pull'
347
+ ? args.includes('--push')
348
+ : !args.includes('--no-sync');
349
+ if (shouldPush) {
306
350
  await syncOne({
307
351
  scope,
308
352
  args,
309
353
  check: false,
310
354
  force: args.includes('--force'),
311
- adoptSourcePath: adopt ? sourcePath : undefined,
312
355
  });
313
356
  }
314
357
  }
358
+ async function overlayCommand(args) {
359
+ const action = args[0];
360
+ const harnessRaw = args[1];
361
+ if (action !== 'set' && action !== 'clear' && action !== 'derive') {
362
+ throw new Error('overlay requires set, derive, or clear');
363
+ }
364
+ const harness = inferHarness(undefined, harnessRaw);
365
+ if (harnessRaw !== harness)
366
+ throw new Error('overlay harness must be claude-code, codex, or opencode');
367
+ const file = action === 'set' ? args[2] : undefined;
368
+ if (action === 'set' && !file)
369
+ throw new Error('overlay set requires a source file');
370
+ const sharedFile = action === 'derive' ? args[2] : undefined;
371
+ const renderedFile = action === 'derive' ? args[3] : undefined;
372
+ if (action === 'derive' && (!sharedFile || !renderedFile)) {
373
+ throw new Error('overlay derive requires <shared-file> <rendered-file>');
374
+ }
375
+ const sourcePath = file ? path.resolve(file) : null;
376
+ if (sourcePath && !fs.existsSync(sourcePath))
377
+ throw new Error(`source file not found: ${sourcePath}`);
378
+ const sharedPath = sharedFile ? path.resolve(sharedFile) : null;
379
+ const renderedPath = renderedFile ? path.resolve(renderedFile) : null;
380
+ if (sharedPath && !fs.existsSync(sharedPath))
381
+ throw new Error(`shared file not found: ${sharedPath}`);
382
+ if (renderedPath && !fs.existsSync(renderedPath))
383
+ throw new Error(`rendered file not found: ${renderedPath}`);
384
+ const scope = inferScope(undefined, flag(args, '--scope'));
385
+ const ctx = context(scope, args);
386
+ const current = await fetchDocument(scope, ctx.projectId, ctx.directory);
387
+ if (!current)
388
+ throw new Error('canonical doctrine does not exist; adopt it first');
389
+ let content = '';
390
+ if (sourcePath) {
391
+ content = fs.readFileSync(sourcePath, 'utf8');
392
+ }
393
+ else if (sharedPath && renderedPath) {
394
+ const shared = fs.readFileSync(sharedPath, 'utf8');
395
+ const rendered = fs.readFileSync(renderedPath, 'utf8');
396
+ if (!rendered.startsWith(shared)) {
397
+ throw new Error('rendered file does not start with shared file bytes; cannot derive exact suffix');
398
+ }
399
+ content = rendered.slice(shared.length);
400
+ }
401
+ const body = await apiJson('/v1/agents/overlay', {
402
+ method: 'POST',
403
+ body: JSON.stringify({
404
+ scope,
405
+ projectId: ctx.projectId,
406
+ directory: ctx.directory,
407
+ sourceHarness: harness,
408
+ sourcePath: renderedPath || sourcePath,
409
+ content,
410
+ overlayHarness: harness,
411
+ expectedRevision: current.revision,
412
+ followers: ctx.targets.map((target) => ({
413
+ harness: target.harness,
414
+ path: target.manifestPath,
415
+ note: target.note,
416
+ })),
417
+ }),
418
+ });
419
+ const verb = action === 'set' ? 'Set' : action === 'derive' ? 'Derived' : 'Cleared';
420
+ console.log(`${verb} ${harness} overlay at revision ${body.document.revision}.`);
421
+ if (!args.includes('--no-sync')) {
422
+ await syncOne({ scope, args, check: false, force: args.includes('--force') });
423
+ }
424
+ }
315
425
  async function syncCommand(args, checkOverride = false) {
316
426
  const requested = flag(args, '--scope') || 'all';
317
427
  if (!['global', 'repo', 'all'].includes(requested)) {
@@ -334,8 +444,8 @@ async function syncCommand(args, checkOverride = false) {
334
444
  }
335
445
  }
336
446
  /** Init-time seed for the active harness. No canon is a clean no-op; only a
337
- * missing/empty follower is written. Every existing file, including a stale
338
- * managed follower, requires an explicit `agents sync`. */
447
+ * missing/empty follower is written. Every existing nonmatching file requires
448
+ * an explicit `agents push` or `agents pull`. */
339
449
  async function bootstrapAgentDoctrine(apiKey, harness) {
340
450
  const result = {
341
451
  canonicalScopes: [], currentFollowers: 0, changedFollowers: 0, blockedFollowers: 0,
@@ -358,10 +468,19 @@ async function runAgents(args) {
358
468
  console.log(HELP);
359
469
  return;
360
470
  }
471
+ if (sub === 'pull')
472
+ return exactSourceCommand(args.slice(1), 'pull');
473
+ if (sub === 'push')
474
+ return syncCommand(args.slice(1));
361
475
  if (sub === 'adopt')
362
- return writeCommand(args.slice(1), true);
363
- if (sub === 'optimize')
364
- return writeCommand(args.slice(1), false);
476
+ return exactSourceCommand(args.slice(1), 'adopt');
477
+ if (sub === 'revise')
478
+ return exactSourceCommand(args.slice(1), 'revise');
479
+ if (sub === 'overlay')
480
+ return overlayCommand(args.slice(1));
481
+ if (sub === 'optimize') {
482
+ throw new Error('agents optimize was removed: use lossless agents pull or explicit agents overlay');
483
+ }
365
484
  if (sub === 'sync')
366
485
  return syncCommand(args.slice(1));
367
486
  if (sub === 'status')