phewsh 0.15.62 → 0.15.64
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/LICENSE +21 -0
- package/README.md +16 -1
- package/SECURITY.md +97 -0
- package/commands/mbhd.js +5 -4
- package/commands/mcp.js +1 -1
- package/commands/sequence.js +14 -0
- package/commands/session.js +57 -13
- package/lib/selfheal.js +1 -0
- package/lib/sequencer/discover.js +30 -2
- package/lib/sequencer/index.js +8 -4
- package/lib/truth.js +36 -1
- package/package.json +8 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 phewsh (hello@phewsh.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,9 +5,24 @@ Turn intent into action. Structure your thinking, execute your next step.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
curl -fsSL https://phewsh.com/install.sh | sh
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
No sudo, nvm-aware. (Prefer a package manager? `npm install -g phewsh` —
|
|
12
|
+
same artifact, built from this repo.)
|
|
13
|
+
|
|
14
|
+
Then let it improve the AI tools you already have — without launching anything:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
phewsh ambient on # your .intent/ + a verified brief reach Claude Code, Codex,
|
|
18
|
+
# Gemini, Cursor at the start of every session (reversible)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
phewsh isn't just a CLI — `ambient` makes every supported tool start aligned to
|
|
22
|
+
what you're building. What it reads, writes, and sends is documented, factually,
|
|
23
|
+
in [SECURITY.md](./SECURITY.md). It's local-first and MIT; this repo is an exact
|
|
24
|
+
mirror of the published npm package.
|
|
25
|
+
|
|
11
26
|
## Quick Start
|
|
12
27
|
|
|
13
28
|
```bash
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Security & Data Flow — phewsh CLI
|
|
2
|
+
|
|
3
|
+
phewsh is a **local-first** tool. It runs on your machine, reads your project's
|
|
4
|
+
`.intent/` and your installed AI tools' config, and helps you carry context
|
|
5
|
+
between those tools. This document states — factually, traceable to source —
|
|
6
|
+
what it reads, writes, and sends. No vague claims; if something here is wrong,
|
|
7
|
+
it's a bug: **hello@phewsh.com**.
|
|
8
|
+
|
|
9
|
+
> Status: this is an honest self-documented inventory, **not** an independent
|
|
10
|
+
> audit. The CLI source is **public and MIT-licensed** at
|
|
11
|
+
> **github.com/cleverIdeaz/phewsh-cli** — an exact mirror of the published npm
|
|
12
|
+
> package, so you can read exactly what you install (`bin/ commands/ lib/ mcp/`).
|
|
13
|
+
> An independent review is still planned (see "Roadmap").
|
|
14
|
+
|
|
15
|
+
## What phewsh reads
|
|
16
|
+
- **`.intent/`** in your project (`vision.md`, `plan.md`, `status.md`, `next.md`,
|
|
17
|
+
`project.json`, `next.json`). This is the source of truth it projects.
|
|
18
|
+
- **Your tools' context files** when present: `CLAUDE.md`, `AGENTS.md`,
|
|
19
|
+
`GEMINI.md`, `.cursorrules`, `.github/copilot-instructions.md`, `README.md`.
|
|
20
|
+
- **Your Claude project memory** (`~/.claude/projects/<cwd>/memory/MEMORY.md` and
|
|
21
|
+
files it links) and **global per-user memory** (`~/.claude/CLAUDE.md`,
|
|
22
|
+
`~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`) — read-only, used to enrich the
|
|
23
|
+
`phewsh seq` summary. Global memory is **never** written into a project file
|
|
24
|
+
unless you pass `--include-global` (`lib/sequencer/index.js`).
|
|
25
|
+
- **Git status** of the current repo (read-only: `git status`, HEAD, diff stats).
|
|
26
|
+
|
|
27
|
+
## What phewsh writes (and where)
|
|
28
|
+
Every generated block is wrapped in `<!-- PHEWSH:START -->` / `<!-- PHEWSH:END -->`
|
|
29
|
+
markers; **content outside the markers is preserved byte-for-byte** (tested).
|
|
30
|
+
- **Project context files** — the canonical `.intent/` projection into
|
|
31
|
+
`CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursorrules` (only files that
|
|
32
|
+
already exist, unless you create them). One canonical projector
|
|
33
|
+
(`lib/selfheal.js`) is the single writer.
|
|
34
|
+
- **`~/.phewsh/`** — `config.json` (your settings/keys, mode **0600**, dir 0700),
|
|
35
|
+
`briefs/` (saved handoff briefs), `sessions/`, `receipts/`, ledger files.
|
|
36
|
+
- **Shell rc** — only if you run `phewsh shim on` (adds one PATH line; reversible
|
|
37
|
+
with `phewsh shim off`).
|
|
38
|
+
- **Global base files** — only if you run `phewsh ambient on`: a marker-wrapped
|
|
39
|
+
block in `~/.claude/CLAUDE.md` etc. for tools you already have. Reversible with
|
|
40
|
+
`phewsh ambient off`.
|
|
41
|
+
- **Claude Code hook** — `phewsh ambient on` registers `SessionStart`/`SessionEnd`
|
|
42
|
+
hooks (context injection + a metadata breadcrumb; **never your transcript**).
|
|
43
|
+
|
|
44
|
+
## Network calls (exactly these)
|
|
45
|
+
phewsh makes **no network calls for its core local features** (sequencing,
|
|
46
|
+
projection, briefs, status). Calls happen only for these named actions:
|
|
47
|
+
- **`api.anthropic.com` / `openrouter.ai` / `api.together.xyz`** — only if you set
|
|
48
|
+
a BYOK key and use `/run` or the API route. Your key goes **directly** to the
|
|
49
|
+
provider; it is **not** proxied through phewsh servers.
|
|
50
|
+
- **`registry.npmjs.org`** — version check for `phewsh update` (notify-only by
|
|
51
|
+
default).
|
|
52
|
+
- **`<project>.supabase.co`** — only if you `/login`: auth + optional cloud sync
|
|
53
|
+
of your `.intent/` (you initiate `push`/`pull`).
|
|
54
|
+
- **`phewsh.com`** — only for `phewsh serve`/cloud-bridge actions you start.
|
|
55
|
+
- **Sustainability telemetry** (`trackSap`, `lib/supabase.js`) — sends **model
|
|
56
|
+
name, token counts, and a kWh estimate only**. No prompts, no responses, no
|
|
57
|
+
file contents. Tied to your account id only when logged in.
|
|
58
|
+
|
|
59
|
+
## Credentials
|
|
60
|
+
- A bring-your-own key lives in `~/.phewsh/config.json` at mode **0600**
|
|
61
|
+
(`lib/config-file.js`) and is sent **directly to the provider**, never to us.
|
|
62
|
+
- phewsh routes through your installed tools on **their own login** (your Claude
|
|
63
|
+
subscription, your ChatGPT plan) — it does not see or store those credentials.
|
|
64
|
+
|
|
65
|
+
## The local bridge
|
|
66
|
+
`phewsh serve` binds **`127.0.0.1` only** (loopback), with an origin allowlist,
|
|
67
|
+
and runs only while you run it.
|
|
68
|
+
|
|
69
|
+
## Known issues / hardening backlog (honest)
|
|
70
|
+
- **Local shell-injection surface (medium, local-only):** a few call sites build
|
|
71
|
+
shell strings from REPL input via `execSync` (e.g. `commands/session.js` gate /
|
|
72
|
+
outcomes arg passthrough). Inputs are your own typed/pasted text on your own
|
|
73
|
+
machine, but crafted args could execute. Fix: convert to argument-safe
|
|
74
|
+
`execFileSync`/`spawnSync`. Tracked as a Next item.
|
|
75
|
+
- **No release provenance yet** — npm provenance, signed tags, and a public
|
|
76
|
+
source repo are planned (Roadmap) so the published artifact is verifiable.
|
|
77
|
+
|
|
78
|
+
## Disable / uninstall
|
|
79
|
+
- `phewsh ambient off` — removes injected hooks and global base files; restores
|
|
80
|
+
your files.
|
|
81
|
+
- `phewsh shim off` — removes the PATH line.
|
|
82
|
+
- `npm uninstall -g phewsh` — removes the CLI. `rm -rf ~/.phewsh` removes local
|
|
83
|
+
state. Generated `PHEWSH:START/END` blocks can be deleted by hand; everything
|
|
84
|
+
outside them is yours and untouched.
|
|
85
|
+
|
|
86
|
+
## Roadmap to verifiable trust
|
|
87
|
+
Done: this doc, exec hardening, telemetry stays counts-only and explicit, and a
|
|
88
|
+
**public MIT source repo** (`cleverIdeaz/phewsh-cli`) mirroring the npm package.
|
|
89
|
+
Before broad promotion: npm provenance, signed tags, and dependency/secret
|
|
90
|
+
scanning so the published artifact is cryptographically verifiable from the
|
|
91
|
+
public source. For enterprise: a documented threat model and an independent
|
|
92
|
+
security assessment. We will not call phewsh "audited" until an independent
|
|
93
|
+
audit has actually occurred.
|
|
94
|
+
|
|
95
|
+
## Reporting
|
|
96
|
+
Security issues: **hello@phewsh.com**. Please allow reasonable time to remediate
|
|
97
|
+
before public disclosure.
|
package/commands/mbhd.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
2
|
|
|
3
3
|
const WEB_URL = 'https://phewsh.com/mbhd';
|
|
4
4
|
|
|
5
5
|
console.log(`\n 🎵 Opening MBHD Music Engine...\n`);
|
|
6
6
|
|
|
7
7
|
try {
|
|
8
|
+
// execFileSync (no shell) — argument-safe even though the URL is constant.
|
|
8
9
|
if (process.platform === 'darwin') {
|
|
9
|
-
|
|
10
|
+
execFileSync('open', [WEB_URL]);
|
|
10
11
|
} else if (process.platform === 'win32') {
|
|
11
|
-
|
|
12
|
+
execFileSync('cmd', ['/c', 'start', '', WEB_URL]);
|
|
12
13
|
} else {
|
|
13
|
-
|
|
14
|
+
execFileSync('xdg-open', [WEB_URL]);
|
|
14
15
|
}
|
|
15
16
|
} catch {
|
|
16
17
|
console.log(` Could not open browser. Visit: ${WEB_URL}\n`);
|
package/commands/mcp.js
CHANGED
|
@@ -48,7 +48,7 @@ function loadLocalProject() {
|
|
|
48
48
|
decisionGate: null,
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
const files = ['vision.md', 'plan.md', 'next.md', 'status.md'
|
|
51
|
+
const files = ['vision.md', 'plan.md', 'next.md', 'status.md'];
|
|
52
52
|
for (const file of files) {
|
|
53
53
|
const filePath = path.join(INTENT_DIR, file);
|
|
54
54
|
if (fs.existsSync(filePath)) {
|
package/commands/sequence.js
CHANGED
|
@@ -117,6 +117,20 @@ async function main() {
|
|
|
117
117
|
// Default behavior: no target = stdout summary
|
|
118
118
|
const target = flags.target || 'stdout';
|
|
119
119
|
|
|
120
|
+
// CANONICAL WRITE. Writing a native projection (e.g. `phewsh seq claude -w`)
|
|
121
|
+
// goes through the one canonical projector (self-heal), NOT the broad
|
|
122
|
+
// sequencer — identical block, identical source policy, no stale clobber.
|
|
123
|
+
if (flags.write && target === 'claude-md') {
|
|
124
|
+
const selfheal = require('../lib/selfheal');
|
|
125
|
+
const res = selfheal.syncContextFiles({ targets: ['CLAUDE.md'], createMissing: true });
|
|
126
|
+
if (res.synced && res.synced.length) {
|
|
127
|
+
console.log(`\n ${green('✓')} ${sage('CLAUDE.md — canonical .intent/ projection (same block self-heal & watch write)')}\n`);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`\n ${slate('CLAUDE.md already current' + (res.reason ? ' (' + res.reason + ')' : ''))}\n`);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
try {
|
|
121
135
|
const result = sequence({
|
|
122
136
|
target,
|
package/commands/session.js
CHANGED
|
@@ -554,6 +554,8 @@ async function main() {
|
|
|
554
554
|
const parts = [cream('HEAD ' + v.shortHead)];
|
|
555
555
|
parts.push(v.dirtyCount ? peach(`${v.dirtyCount} uncommitted`) : sage('clean'));
|
|
556
556
|
if (v.driftCommits > 0) parts.push(ember(`⚠ ${v.driftCommits} commit${v.driftCommits !== 1 ? 's' : ''} since .intent updated`));
|
|
557
|
+
// Sharper than recency: the docs still name an older version than shipped.
|
|
558
|
+
if (v.versionDrift) parts.push(ember(`⚠ .intent says ${v.versionDrift.claimed}, shipped ${v.versionDrift.shipped} — /reconcile`));
|
|
557
559
|
row('VERIFIED', parts.join(slate(' · ')) + slate(' — /truth · /brief'));
|
|
558
560
|
}
|
|
559
561
|
} catch { /* the verified row is a glance, never a blocker */ }
|
|
@@ -1712,7 +1714,6 @@ async function main() {
|
|
|
1712
1714
|
console.log(` ${teal('/key')} ${sage('Set API key (optional — harnesses need none)')}`);
|
|
1713
1715
|
console.log(` ${teal('/login')} ${sage('Identity + cloud sync')}`);
|
|
1714
1716
|
console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model — passed through, the provider validates')}`);
|
|
1715
|
-
console.log(` ${teal('/width')} ${slate('[n]')} ${sage('Fix mid-word wrapping if your terminal misreports its size')}`);
|
|
1716
1717
|
console.log(` ${teal('/update')} ${sage('Update phewsh')}`);
|
|
1717
1718
|
console.log(` ${teal('/tour')} ${sage('Quick walkthrough')}`);
|
|
1718
1719
|
console.log('');
|
|
@@ -1925,9 +1926,11 @@ async function main() {
|
|
|
1925
1926
|
|
|
1926
1927
|
if (cmd === 'gate') {
|
|
1927
1928
|
try {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1929
|
+
// execFileSync with an argv array — no shell, so REPL input can never
|
|
1930
|
+
// be interpreted as shell metacharacters (hardening: was execSync).
|
|
1931
|
+
const { execFileSync } = require('child_process');
|
|
1932
|
+
const gateArgs = (cmdArg || 'status').split(/\s+/).filter(Boolean);
|
|
1933
|
+
execFileSync(process.execPath, [path.join(__dirname, 'gate.js'), ...gateArgs], { stdio: 'inherit' });
|
|
1931
1934
|
intentFiles = loadIntentContext();
|
|
1932
1935
|
systemPrompt = buildSystemPrompt(intentFiles);
|
|
1933
1936
|
} catch (err) {
|
|
@@ -1940,17 +1943,43 @@ async function main() {
|
|
|
1940
1943
|
if (cmd === 'seq' || cmd === 'sequence') {
|
|
1941
1944
|
try {
|
|
1942
1945
|
const { sequence } = require('../lib/sequencer');
|
|
1946
|
+
const { resolveProjectRoot, discover } = require('../lib/sequencer/discover');
|
|
1943
1947
|
const target = cmdArg?.split(/\s+/)[0];
|
|
1944
1948
|
const explain = cmdArg?.includes('explain') || cmdArg?.includes('-e');
|
|
1945
|
-
const write = cmdArg?.includes('-w') || cmdArg?.includes('write');
|
|
1946
1949
|
|
|
1947
1950
|
if (target === 'claude' || target === 'claude-md') {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
+
// CANONICAL WRITE. /seq claude writes the SAME deterministic block as
|
|
1952
|
+
// self-heal and watch — one source policy (.intent canonical), one
|
|
1953
|
+
// renderer — so a manual sequence can never produce a divergent or
|
|
1954
|
+
// stale CLAUDE.md. (Previously this called the broad sequencer, which
|
|
1955
|
+
// ingested narrative.md + memory logs and clobbered the canonical block.)
|
|
1956
|
+
const res = selfheal.syncContextFiles({ targets: ['CLAUDE.md'], createMissing: true });
|
|
1957
|
+
if (res.synced && res.synced.length) {
|
|
1958
|
+
console.log(`\n ${teal('●')} ${sage('CLAUDE.md updated — canonical .intent/ projection')} ${slate('(same block self-heal & watch write)')}\n`);
|
|
1959
|
+
} else {
|
|
1960
|
+
console.log(`\n ${slate('CLAUDE.md already current' + (res.reason ? ' (' + res.reason + ')' : ''))}\n`);
|
|
1961
|
+
}
|
|
1962
|
+
} else if (explain || target === 'sources' || target === 'root') {
|
|
1963
|
+
// Diagnostic: show what the sequencer sees — resolved root, selected
|
|
1964
|
+
// sources (with scope), and the canonical projection's restricted set.
|
|
1965
|
+
const root = resolveProjectRoot(process.cwd());
|
|
1966
|
+
const found = discover(process.cwd());
|
|
1967
|
+
console.log('');
|
|
1968
|
+
console.log(` ${b(cream('Sequence diagnostics'))}`);
|
|
1969
|
+
ui.divider('line');
|
|
1970
|
+
console.log(` ${sage('resolved root')} ${cream(root)}${root !== process.cwd() ? slate(' (walked up from ' + process.cwd() + ')') : ''}`);
|
|
1971
|
+
console.log(` ${sage('canonical write set')} ${slate('vision.md · project.json · status.md · next.md · next.json')}`);
|
|
1972
|
+
console.log(` ${sage('broad read (this preview)')}`);
|
|
1973
|
+
for (const s of found) {
|
|
1974
|
+
const tag = s.scope === 'global' ? slate('global ') : sage('project');
|
|
1975
|
+
const canon = ['vision.md','project.json','status.md','next.md','next.json'].includes(s.name) ? teal(' ✓ canonical') : slate(' · preview-only');
|
|
1976
|
+
console.log(` ${tag} ${cream(s.name.padEnd(20))}${canon}`);
|
|
1951
1977
|
}
|
|
1978
|
+
ui.divider('line');
|
|
1979
|
+
console.log(` ${slate('write the canonical projection with')} ${cream('/seq claude')}`);
|
|
1980
|
+
console.log('');
|
|
1952
1981
|
} else {
|
|
1953
|
-
// Default: stdout
|
|
1982
|
+
// Default: broad, READ-ONLY synthesis to stdout — never writes a file.
|
|
1954
1983
|
sequence({ target: 'stdout', explain });
|
|
1955
1984
|
}
|
|
1956
1985
|
} catch (err) {
|
|
@@ -2544,10 +2573,11 @@ async function main() {
|
|
|
2544
2573
|
|
|
2545
2574
|
if (cmd === 'outcomes') {
|
|
2546
2575
|
try {
|
|
2547
|
-
//
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2576
|
+
// execFileSync (argv array, no shell) so the labeling prompt owns
|
|
2577
|
+
// stdin AND REPL input can't be shell-interpreted (hardening).
|
|
2578
|
+
const { execFileSync } = require('child_process');
|
|
2579
|
+
const outcomesArgs = cmdArg ? cmdArg.split(/\s+/).filter(Boolean) : [];
|
|
2580
|
+
execFileSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), 'outcomes', ...outcomesArgs], { stdio: 'inherit' });
|
|
2551
2581
|
} catch { /* user quit mid-labeling — fine */ }
|
|
2552
2582
|
rl.prompt();
|
|
2553
2583
|
return;
|
|
@@ -2725,6 +2755,20 @@ async function main() {
|
|
|
2725
2755
|
console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('No changes detected. 1 kept · 2 undid · 3 redid · 4 flopped · /reconcile · /switch <tool>')}`);
|
|
2726
2756
|
}
|
|
2727
2757
|
console.log(` ${slate('continuing elsewhere?')} ${cream('/handoff')} ${slate('shows the updated brief the next tool inherits')}`);
|
|
2758
|
+
// Auto-reconcile OFFER (human-gated, never auto-writes). Only when the
|
|
2759
|
+
// work left project UNDERSTANDING stale — the docs now disagree with the
|
|
2760
|
+
// code or a projection drifted. Ordinary implementation diffs and a merely
|
|
2761
|
+
// dirty tree do NOT trigger this; that would be noise.
|
|
2762
|
+
try {
|
|
2763
|
+
const truthDrift = (postflight.conflicts || []).filter(c =>
|
|
2764
|
+
/current-state intent disagrees|current-state claims may be stale|should be regenerated|before authoritative intent updated/i.test(c));
|
|
2765
|
+
if (truthDrift.length) {
|
|
2766
|
+
console.log('');
|
|
2767
|
+
console.log(` ${ember('⚠')} ${sage('Your project understanding may now be stale:')}`);
|
|
2768
|
+
console.log(` ${slate(truthDrift[0])}`);
|
|
2769
|
+
console.log(` ${sage('Bring the record up to date so the next tool inherits the truth →')} ${cream('/reconcile')} ${slate('(proposes an exact diff; writes only after you approve)')}`);
|
|
2770
|
+
}
|
|
2771
|
+
} catch { /* the offer is a nicety, never a blocker */ }
|
|
2728
2772
|
console.log('');
|
|
2729
2773
|
rl.prompt();
|
|
2730
2774
|
return;
|
package/lib/selfheal.js
CHANGED
|
@@ -205,6 +205,7 @@ function projectionStatus({ cwd = process.cwd(), targets = TARGET_FILES } = {})
|
|
|
205
205
|
// `seq`), where the user is actually working in the project.
|
|
206
206
|
function syncContextFiles({ cwd = process.cwd(), targets = TARGET_FILES, createMissing = true } = {}) {
|
|
207
207
|
try {
|
|
208
|
+
cwd = require('./sequencer/discover').resolveProjectRoot(cwd);
|
|
208
209
|
const intentDir = path.join(cwd, '.intent');
|
|
209
210
|
if (!fs.existsSync(intentDir)) return { synced: [], reason: 'no-intent' };
|
|
210
211
|
const core = buildContextCore(cwd);
|
|
@@ -15,10 +15,38 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const os = require('os');
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// narrative.md is intentionally NOT a source: it is a deprecated artifact that
|
|
19
|
+
// predates the four-word model (Project · Next · Work · Record) and is no longer
|
|
20
|
+
// authoritative project truth. It must never enter a native projection. See
|
|
21
|
+
// resolveProjectRoot + the canonical projection in selfheal.
|
|
22
|
+
const INTENT_FILES = ['vision.md', 'plan.md', 'status.md', 'next.md'];
|
|
19
23
|
const INTENT_JSON = ['project.json', 'next.json', 'pps.json', 'gate.json'];
|
|
20
24
|
|
|
25
|
+
// Resolve the project root by walking UP from `start` to the nearest ancestor
|
|
26
|
+
// (including start) that contains a `.intent/` directory — bounded by the home
|
|
27
|
+
// dir and the filesystem root. This stops a nested dir (e.g. an app folder under
|
|
28
|
+
// a monorepo) from being treated as its own project, and makes every
|
|
29
|
+
// context-generating path agree on the same root. Returns `start` unchanged when
|
|
30
|
+
// no `.intent/` exists anywhere upward (so bare-directory behavior is untouched).
|
|
31
|
+
function resolveProjectRoot(start = process.cwd(), home = os.homedir()) {
|
|
32
|
+
let dir;
|
|
33
|
+
try { dir = path.resolve(start); } catch { return start; }
|
|
34
|
+
const fsRoot = path.parse(dir).root;
|
|
35
|
+
const homeReal = (() => { try { return fs.realpathSync(home); } catch { return home; } })();
|
|
36
|
+
while (true) {
|
|
37
|
+
if (fs.existsSync(path.join(dir, '.intent'))) return dir;
|
|
38
|
+
if (dir === fsRoot) break;
|
|
39
|
+
let real; try { real = fs.realpathSync(dir); } catch { real = dir; }
|
|
40
|
+
if (real === homeReal) break; // don't escape above the user's home
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir) break;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
return path.resolve(start);
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
function discover(cwd = process.cwd(), home = os.homedir()) {
|
|
49
|
+
cwd = resolveProjectRoot(cwd, home);
|
|
22
50
|
const sources = [];
|
|
23
51
|
const seenPaths = new Set();
|
|
24
52
|
|
|
@@ -119,4 +147,4 @@ function discover(cwd = process.cwd(), home = os.homedir()) {
|
|
|
119
147
|
return sources;
|
|
120
148
|
}
|
|
121
149
|
|
|
122
|
-
module.exports = { discover };
|
|
150
|
+
module.exports = { discover, resolveProjectRoot };
|
package/lib/sequencer/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// The core pipeline: discover → parse → rank → compress → emit
|
|
4
4
|
// One function. N inputs. 1 optimal output per target format.
|
|
5
5
|
|
|
6
|
-
const { discover } = require('./discover');
|
|
6
|
+
const { discover, resolveProjectRoot } = require('./discover');
|
|
7
7
|
const { rank } = require('./ranker');
|
|
8
8
|
const { compress } = require('./compressor');
|
|
9
9
|
|
|
@@ -53,8 +53,12 @@ function sequence(options = {}) {
|
|
|
53
53
|
cwd = process.cwd(),
|
|
54
54
|
} = options;
|
|
55
55
|
|
|
56
|
+
// Resolve to the canonical project root once, so discovery, naming, and any
|
|
57
|
+
// file write all agree on the same project (a nested dir never self-resolves).
|
|
58
|
+
const root = resolveProjectRoot(cwd);
|
|
59
|
+
|
|
56
60
|
// 1. Discover all source files (project + global)
|
|
57
|
-
let sources = discover(
|
|
61
|
+
let sources = discover(root);
|
|
58
62
|
|
|
59
63
|
// Privacy guard: global per-user memory enriches the summary (stdout), but
|
|
60
64
|
// never bleeds into a project-file target (CLAUDE.md) unless asked for —
|
|
@@ -112,12 +116,12 @@ function sequence(options = {}) {
|
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
const output = emitter.emit(chunks, {
|
|
115
|
-
projectName: getProjectName(chunks,
|
|
119
|
+
projectName: getProjectName(chunks, root),
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
// Write to file if requested
|
|
119
123
|
if (write && emitter.writeToFile) {
|
|
120
|
-
const result = emitter.writeToFile(output,
|
|
124
|
+
const result = emitter.writeToFile(output, root);
|
|
121
125
|
return { chunks, output, sources, writeResult: result };
|
|
122
126
|
}
|
|
123
127
|
|
package/lib/truth.js
CHANGED
|
@@ -273,16 +273,50 @@ async function fetchNpmLatest(packageName, { fetchImpl = global.fetch, timeoutMs
|
|
|
273
273
|
// Fast, offline, fail-soft snapshot for the front-door cockpit — no npm fetch,
|
|
274
274
|
// no file hashing. Answers "what verified truth is loaded?" at a glance so the
|
|
275
275
|
// product thesis (one verified truth) is visible the moment phewsh opens.
|
|
276
|
+
// Cheap, offline version-claim drift: does the shipped package version sit ahead
|
|
277
|
+
// of the newest version the .intent narrative still claims? This is the SHARP
|
|
278
|
+
// staleness signal the front door was missing — the recency-based "commits since
|
|
279
|
+
// .intent changed" check goes quiet the moment you edit status.md, even if its
|
|
280
|
+
// headline claim is stale. We compare the project's package version (repo root,
|
|
281
|
+
// or a cli/ monorepo child like phewsh itself) to the max version mentioned in
|
|
282
|
+
// status.md/next.md, and only flag when code has shipped PAST the docs.
|
|
283
|
+
function quickVersionDrift(cwd = process.cwd()) {
|
|
284
|
+
try {
|
|
285
|
+
let shipped = null;
|
|
286
|
+
for (const rel of ['package.json', 'cli/package.json']) {
|
|
287
|
+
const p = path.join(cwd, rel);
|
|
288
|
+
if (!fs.existsSync(p)) continue;
|
|
289
|
+
try { shipped = JSON.parse(fs.readFileSync(p, 'utf-8')).version; } catch { /* unreadable */ }
|
|
290
|
+
if (shipped) break;
|
|
291
|
+
}
|
|
292
|
+
if (!shipped) return null;
|
|
293
|
+
let claimed = null;
|
|
294
|
+
for (const rel of ['.intent/status.md', '.intent/next.md']) {
|
|
295
|
+
const p = path.join(cwd, rel);
|
|
296
|
+
if (!fs.existsSync(p)) continue;
|
|
297
|
+
for (const m of fs.readFileSync(p, 'utf-8').matchAll(/\bv?(\d+\.\d+\.\d+)\b/g)) {
|
|
298
|
+
if (!claimed || compareVersions(m[1], claimed) > 0) claimed = m[1];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!claimed) return null;
|
|
302
|
+
return compareVersions(shipped, claimed) > 0 ? { shipped, claimed } : null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
276
308
|
function quickVerifiedState(cwd = process.cwd()) {
|
|
277
309
|
try {
|
|
278
310
|
const git = gitSnapshot(cwd);
|
|
279
|
-
|
|
311
|
+
const versionDrift = quickVersionDrift(cwd);
|
|
312
|
+
if (!git.available) return { available: false, isRepo: false, versionDrift };
|
|
280
313
|
return {
|
|
281
314
|
available: true,
|
|
282
315
|
isRepo: true,
|
|
283
316
|
shortHead: git.shortHead,
|
|
284
317
|
dirtyCount: git.tracked.length + git.untracked.length,
|
|
285
318
|
driftCommits: git.drift?.tracked ? git.drift.commitsSince : 0,
|
|
319
|
+
versionDrift,
|
|
286
320
|
};
|
|
287
321
|
} catch {
|
|
288
322
|
return { available: false, isRepo: false };
|
|
@@ -397,5 +431,6 @@ module.exports = {
|
|
|
397
431
|
parsePorcelain,
|
|
398
432
|
projectionInfo,
|
|
399
433
|
quickVerifiedState,
|
|
434
|
+
quickVersionDrift,
|
|
400
435
|
statusDrift,
|
|
401
436
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phewsh",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.64",
|
|
4
4
|
"description": "Turn intent into action. Structure your thinking, execute your next step.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"phewsh": "bin/phewsh.js"
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"commands/",
|
|
11
11
|
"lib/",
|
|
12
12
|
"mcp/",
|
|
13
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
"SECURITY.md",
|
|
15
|
+
"LICENSE"
|
|
14
16
|
],
|
|
15
17
|
"keywords": [
|
|
16
18
|
"intent",
|
|
@@ -29,7 +31,10 @@
|
|
|
29
31
|
"license": "MIT",
|
|
30
32
|
"repository": {
|
|
31
33
|
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/cleverIdeaz/phewsh.git"
|
|
34
|
+
"url": "git+https://github.com/cleverIdeaz/phewsh-cli.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/cleverIdeaz/phewsh-cli/issues"
|
|
33
38
|
},
|
|
34
39
|
"homepage": "https://phewsh.com",
|
|
35
40
|
"engines": {
|