phewsh 0.15.34 → 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 +30 -2
- package/commands/session.js +3 -3
- package/lib/selfheal.js +74 -0
- package/lib/sequencer/parsers/generic.js +8 -1
- package/package.json +1 -1
package/commands/hook.js
CHANGED
|
@@ -111,6 +111,28 @@ function sessionStart() {
|
|
|
111
111
|
const cont = continuityBrief(path.basename(process.cwd()));
|
|
112
112
|
if (cont) parts.push(`\n## Continuity (across your tools)\n${cont}`);
|
|
113
113
|
|
|
114
|
+
// Drift nudge (read-only): if code moved ahead of .intent/, say so — right
|
|
115
|
+
// here, where the agent will see it — so the human knows *when* to reconcile.
|
|
116
|
+
// We never auto-rewrite their source of truth; we just make the gap visible.
|
|
117
|
+
try {
|
|
118
|
+
const { statusDrift } = require('../lib/truth');
|
|
119
|
+
const drift = statusDrift(process.cwd());
|
|
120
|
+
if (drift && drift.tracked && drift.commitsSince > 0) {
|
|
121
|
+
parts.push(`\n## ⚠ phewsh drift\n.intent/ is ${drift.commitsSince} commit(s) behind the code (since ${drift.lastCommit}) — its current-state claims may be stale. Good moment to reconcile so every tool inherits today's reality.`);
|
|
122
|
+
}
|
|
123
|
+
} catch { /* drift is a nicety; never break the host */ }
|
|
124
|
+
|
|
125
|
+
// Ambient operating guidance: make phewsh *felt*, not invoked. The human may
|
|
126
|
+
// not know a single slash command — translate intent into phewsh actions for
|
|
127
|
+
// them, nudge gently, and leave a quiet signature so they feel the layer.
|
|
128
|
+
parts.push(`\n## Working with phewsh installed (ambient guidance)
|
|
129
|
+
phewsh keeps this project's intent, decisions, and outcomes aligned across every AI tool — so nothing is lost when the human switches tools. Help it work *for* them without making them learn it:
|
|
130
|
+
- Do their actual request first, and well.
|
|
131
|
+
- When you make a real decision, ship something, or change direction, offer in plain language to keep the shared record current — e.g. "want me to reconcile this into phewsh so your other tools inherit it?" Never make them memorize slash commands; map their intent to phewsh actions yourself.
|
|
132
|
+
- If the drift note above is present, mention it once, gently.
|
|
133
|
+
- Keep nudges subtle and occasional — never nag, never block the work.
|
|
134
|
+
- When phewsh context shaped your reply, sign off quietly with 😮💨🤫 (e.g. "— 😮💨🤫") so the human feels the continuity layer working in the background.`);
|
|
135
|
+
|
|
114
136
|
parts.push(`\n(Brief injected by PHEWSH ambient from .intent/. Honor the constraints above. The human can run \`phewsh\` for mission control — council, outcomes, the decision record.)`);
|
|
115
137
|
|
|
116
138
|
process.stdout.write(parts.join('\n') + '\n');
|
|
@@ -129,11 +151,17 @@ function sessionEnd() {
|
|
|
129
151
|
// or any agent) reads today's intent without anyone running `seq -w`. Silent
|
|
130
152
|
// and deterministic; failures never touch the host tool.
|
|
131
153
|
let healed = false;
|
|
154
|
+
let synced = [];
|
|
132
155
|
try {
|
|
133
156
|
const selfheal = require('../lib/selfheal');
|
|
134
|
-
|
|
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;
|
|
135
163
|
} catch { /* a hook must never error the host */ }
|
|
136
|
-
appendBreadcrumb('session-end', { ...(reason ? { reason } : {}), ...(
|
|
164
|
+
appendBreadcrumb('session-end', { ...(reason ? { reason } : {}), ...(synced.length ? { synced } : {}) });
|
|
137
165
|
process.exit(0);
|
|
138
166
|
});
|
|
139
167
|
// If the host never closes stdin, don't hang it.
|
package/commands/session.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
}
|
|
@@ -2422,7 +2422,7 @@ async function main() {
|
|
|
2422
2422
|
const preflightTruth = await auditTruth();
|
|
2423
2423
|
const before = captureSnapshot(preflightTruth);
|
|
2424
2424
|
const generatedBrief = await generateBrief({ report: preflightTruth });
|
|
2425
|
-
const launchBrief = `${generatedBrief.content}\n\nYou are receiving a PHEWSH handoff brief — acknowledge in one short line that you've loaded it, then proceed. Use it as project context, preserve native tool behavior, and verify claims against the repository before acting. When you finish, exit this tool — PHEWSH resumes, runs an automatic postflight comparing what changed against this brief, and offers reconciliation.`;
|
|
2425
|
+
const launchBrief = `${generatedBrief.content}\n\nYou are receiving a PHEWSH handoff brief — acknowledge in one short line that you've loaded it, then proceed. Use it as project context, preserve native tool behavior, and verify claims against the repository before acting. When you finish, exit this tool — PHEWSH resumes, runs an automatic postflight comparing what changed against this brief, and offers reconciliation.\n\nAmbient guidance: the human may not know any phewsh commands — make the layer felt, not invoked. Do their request first; when you make a real decision, ship something, or change direction, offer in plain language to keep the shared record current ("want me to reconcile this into phewsh so your other tools inherit it?"). Keep it subtle, never nag. When phewsh context shaped your reply, sign off quietly with 😮💨🤫 so they feel the continuity layer working.`;
|
|
2426
2426
|
const savedBrief = persistBrief(generatedBrief.content, { project: projectName, route: target });
|
|
2427
2427
|
const launch = interactiveLaunchArgs(target, launchBrief, { model: harnessModel });
|
|
2428
2428
|
// Foolproof fallback: the brief on the clipboard survives the native
|
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();
|