phewsh 0.15.35 → 0.15.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/hook.js CHANGED
@@ -151,11 +151,17 @@ function sessionEnd() {
151
151
  // or any agent) reads today's intent without anyone running `seq -w`. Silent
152
152
  // and deterministic; failures never touch the host tool.
153
153
  let healed = false;
154
+ let synced = [];
154
155
  try {
155
156
  const selfheal = require('../lib/selfheal');
156
- if (selfheal.isStale()) healed = selfheal.heal().healed;
157
+ // Keep EVERY tool's native context file current from .intent/, not just
158
+ // CLAUDE.md — so the next tool to open here (Codex, Gemini, Cursor, …)
159
+ // reads today's truth without anyone running a command. Idempotent:
160
+ // only writes files whose substantive content actually changed.
161
+ synced = selfheal.syncContextFiles().synced || [];
162
+ healed = synced.length > 0;
157
163
  } catch { /* a hook must never error the host */ }
158
- appendBreadcrumb('session-end', { ...(reason ? { reason } : {}), ...(healed ? { healed: true } : {}) });
164
+ appendBreadcrumb('session-end', { ...(reason ? { reason } : {}), ...(synced.length ? { synced } : {}) });
159
165
  process.exit(0);
160
166
  });
161
167
  // If the host never closes stdin, don't hang it.
@@ -1108,11 +1108,11 @@ async function main() {
1108
1108
  if (/^(y|yes)$/i.test(input.trim())) {
1109
1109
  const r = applyReconciliation(proposal);
1110
1110
  if (r.written) {
1111
- const healed = selfheal.heal({ force: true });
1111
+ const synced = (selfheal.syncContextFiles().synced) || [];
1112
1112
  intentFiles = loadIntentContext();
1113
1113
  systemPrompt = buildSystemPrompt(intentFiles);
1114
1114
  console.log(` ${green('✓')} ${sage('Applied the approved diff to ' + r.target + '.')}`);
1115
- console.log(` ${slate(healed.healed ? 'Generated Claude context refreshed from the approved intent change.' : 'Intent updated; generated context did not require a write.')}`);
1115
+ console.log(` ${slate(synced.length ? 'Synced every tool from the approved change: ' + synced.join(', ') + '.' : 'Intent updated; tool context already current.')}`);
1116
1116
  } else {
1117
1117
  console.log(` ${ember('!')} ${sage('Reconciliation was not applied: ' + r.reason)}`);
1118
1118
  }
package/lib/selfheal.js CHANGED
@@ -76,6 +76,79 @@ function heal({ cwd = process.cwd(), force = false } = {}) {
76
76
  }
77
77
  }
78
78
 
79
+ // ── Multi-tool context sync ──────────────────────────────────────────────
80
+ // phewsh keeps EVERY harness's native context file current from .intent/, not
81
+ // just CLAUDE.md — so opening Codex (AGENTS.md), Gemini (GEMINI.md), or Cursor
82
+ // (.cursorrules) in a phewsh project reads the same truth Claude Code does.
83
+ // Same proven marker pattern (block between PHEWSH:START/END, rest preserved),
84
+ // plus a verifiable signed footer so "is it current?" is `cat`-able, not vibes.
85
+ const TARGET_FILES = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules'];
86
+ const START_MARKER = '<!-- PHEWSH:START -->';
87
+ const END_MARKER = '<!-- PHEWSH:END -->';
88
+
89
+ function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
90
+
91
+ // Strip the volatile signed footer so timestamp-only changes don't cause churn.
92
+ function coreOf(text) {
93
+ return text.replace(/\n?>\s*—\s*synced by phewsh[\s\S]*$/m, '').trim();
94
+ }
95
+
96
+ // Insert/replace the phewsh block between markers; create the file (with a
97
+ // short header) if absent; preserve everything outside the markers. Writes
98
+ // only when the substantive content changed (footer timestamp is ignored), so
99
+ // re-running every session doesn't churn files. Returns true if written.
100
+ function upsertBlock(filePath, core, footer, fileLabel) {
101
+ try {
102
+ const block = core.replace(/\s*$/, '') + '\n' + footer;
103
+ const wrapped = `${START_MARKER}\n${block}\n${END_MARKER}`;
104
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null;
105
+ if (existing && existing.includes(START_MARKER) && existing.includes(END_MARKER)) {
106
+ const re = new RegExp(escapeRe(START_MARKER) + '([\\s\\S]*?)' + escapeRe(END_MARKER));
107
+ const m = existing.match(re);
108
+ if (m && coreOf(m[1]) === coreOf(core)) return false; // unchanged but for the timestamp
109
+ const next = existing.replace(re, wrapped);
110
+ fs.writeFileSync(filePath, next);
111
+ } else if (existing) {
112
+ fs.writeFileSync(filePath, existing.replace(/\s*$/, '') + '\n\n' + wrapped + '\n');
113
+ } else {
114
+ const header = `# ${fileLabel} — kept current by phewsh\n> Project context compiled from .intent/. The block below is auto-managed; edit outside the markers.\n\n`;
115
+ fs.writeFileSync(filePath, header + wrapped + '\n');
116
+ }
117
+ return true;
118
+ } catch { return false; }
119
+ }
120
+
121
+ // Build the phewsh block once (from .intent/ via the sequencer) and write it
122
+ // into every target tool's context file with a signed, timestamped footer.
123
+ function syncContextFiles({ cwd = process.cwd(), targets = TARGET_FILES } = {}) {
124
+ try {
125
+ const intentDir = path.join(cwd, '.intent');
126
+ if (!fs.existsSync(intentDir)) return { synced: [], reason: 'no-intent' };
127
+ const { sequence } = require('./sequencer');
128
+ const prev = process.cwd();
129
+ let output;
130
+ try {
131
+ if (cwd !== prev) process.chdir(cwd);
132
+ output = sequence({ target: 'claude-md', write: false }).output;
133
+ } finally {
134
+ if (cwd !== prev) { try { process.chdir(prev); } catch { /* best-effort */ } }
135
+ }
136
+ if (!output) return { synced: [], reason: 'no-output' };
137
+ let head = '';
138
+ try {
139
+ head = require('child_process').execFileSync('git', ['rev-parse', '--short', 'HEAD'],
140
+ { cwd, encoding: 'utf-8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
141
+ } catch { /* not a git repo — fine */ }
142
+ const stamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
143
+ const footer = `> — synced by phewsh 😮‍💨🤫 · ${stamp}${head ? ' · ' + head : ''}`;
144
+ const synced = [];
145
+ for (const file of targets) {
146
+ if (upsertBlock(path.join(cwd, file), output, footer, file)) synced.push(file);
147
+ }
148
+ return { synced };
149
+ } catch (err) { return { synced: [], reason: err && err.message ? err.message : 'error' }; }
150
+ }
151
+
79
152
  // The deeper drift: code ships but .intent/ content never gets updated (the
80
153
  // exact failure that ate phewsh's own dogfood — 16 versions shipped while
81
154
  // next.md stayed days stale). Count git commits authored AFTER the newest
@@ -166,4 +239,5 @@ function appendToNext(block, { cwd = process.cwd(), file = 'next.md' } = {}) {
166
239
  module.exports = {
167
240
  isStale, heal, newestIntentMs, newestNarrativeMs,
168
241
  commitsSinceIntent, worktreeChanges, wrapDraft, appendToNext,
242
+ syncContextFiles, TARGET_FILES,
169
243
  };
@@ -11,8 +11,15 @@ const TYPE_TO_KIND = {
11
11
  readme: 'context', // README is broad project context
12
12
  };
13
13
 
14
+ // Strip phewsh's own auto-managed block so the sequencer never re-ingests its
15
+ // own output (AGENTS.md / GEMINI.md / .cursorrules are both written by phewsh
16
+ // AND read as sources — without this, each sync perturbs the next = churn loop).
17
+ function stripPhewshBlock(text) {
18
+ return text.replace(/<!--\s*PHEWSH:START\s*-->[\s\S]*?<!--\s*PHEWSH:END\s*-->/g, '').trim();
19
+ }
20
+
14
21
  function parse(source) {
15
- const content = fs.readFileSync(source.path, 'utf-8');
22
+ const content = stripPhewshBlock(fs.readFileSync(source.path, 'utf-8'));
16
23
  if (!content.trim()) return [];
17
24
 
18
25
  const mtime = fs.statSync(source.path).mtime.toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.35",
3
+ "version": "0.15.36",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"