@productbrain/cli 0.1.0-beta.109 → 0.1.0-beta.1096
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/dist/__tests__/audit.test.js +5 -0
- package/dist/__tests__/audit.test.js.map +1 -1
- package/dist/__tests__/config.test.js +272 -2
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/handshake-augment.test.d.ts +2 -0
- package/dist/__tests__/handshake-augment.test.d.ts.map +1 -0
- package/dist/__tests__/handshake-augment.test.js +423 -0
- package/dist/__tests__/handshake-augment.test.js.map +1 -0
- package/dist/__tests__/handshake-dormancy.test.d.ts +2 -0
- package/dist/__tests__/handshake-dormancy.test.d.ts.map +1 -0
- package/dist/__tests__/handshake-dormancy.test.js +207 -0
- package/dist/__tests__/handshake-dormancy.test.js.map +1 -0
- package/dist/__tests__/handshake-formatter.test.d.ts +2 -0
- package/dist/__tests__/handshake-formatter.test.d.ts.map +1 -0
- package/dist/__tests__/handshake-formatter.test.js +67 -0
- package/dist/__tests__/handshake-formatter.test.js.map +1 -0
- package/dist/__tests__/handshake-preview.test.js +65 -3
- package/dist/__tests__/handshake-preview.test.js.map +1 -1
- package/dist/__tests__/handshake.e2e.test.js +998 -3
- package/dist/__tests__/handshake.e2e.test.js.map +1 -1
- package/dist/__tests__/handshake.test.js +4 -4
- package/dist/__tests__/handshake.test.js.map +1 -1
- package/dist/__tests__/notice-marker.test.d.ts +2 -0
- package/dist/__tests__/notice-marker.test.d.ts.map +1 -0
- package/dist/__tests__/notice-marker.test.js +41 -0
- package/dist/__tests__/notice-marker.test.js.map +1 -0
- package/dist/__tests__/onboarding-path-b.test.js +4 -4
- package/dist/__tests__/onboarding-path-b.test.js.map +1 -1
- package/dist/__tests__/orient.test.js +52 -2
- package/dist/__tests__/orient.test.js.map +1 -1
- package/dist/__tests__/perimeter.test.js +23 -1
- package/dist/__tests__/perimeter.test.js.map +1 -1
- package/dist/__tests__/preview-key-refresh.test.d.ts +2 -0
- package/dist/__tests__/preview-key-refresh.test.d.ts.map +1 -0
- package/dist/__tests__/preview-key-refresh.test.js +126 -0
- package/dist/__tests__/preview-key-refresh.test.js.map +1 -0
- package/dist/__tests__/profiles.test.js +106 -2
- package/dist/__tests__/profiles.test.js.map +1 -1
- package/dist/__tests__/proof-run.test.d.ts +2 -0
- package/dist/__tests__/proof-run.test.d.ts.map +1 -0
- package/dist/__tests__/proof-run.test.js +255 -0
- package/dist/__tests__/proof-run.test.js.map +1 -0
- package/dist/__tests__/session-reset.test.d.ts +2 -0
- package/dist/__tests__/session-reset.test.d.ts.map +1 -0
- package/dist/__tests__/session-reset.test.js +122 -0
- package/dist/__tests__/session-reset.test.js.map +1 -0
- package/dist/__tests__/session-resume-backstop.test.d.ts +2 -0
- package/dist/__tests__/session-resume-backstop.test.d.ts.map +1 -0
- package/dist/__tests__/session-resume-backstop.test.js +97 -0
- package/dist/__tests__/session-resume-backstop.test.js.map +1 -0
- package/dist/__tests__/session-start-key-refresh.test.d.ts +2 -0
- package/dist/__tests__/session-start-key-refresh.test.d.ts.map +1 -0
- package/dist/__tests__/session-start-key-refresh.test.js +178 -0
- package/dist/__tests__/session-start-key-refresh.test.js.map +1 -0
- package/dist/__tests__/session-state-machine.test.js +45 -1
- package/dist/__tests__/session-state-machine.test.js.map +1 -1
- package/dist/__tests__/session-switch.test.d.ts +2 -0
- package/dist/__tests__/session-switch.test.d.ts.map +1 -0
- package/dist/__tests__/session-switch.test.js +130 -0
- package/dist/__tests__/session-switch.test.js.map +1 -0
- package/dist/__tests__/update-check.test.js +160 -1
- package/dist/__tests__/update-check.test.js.map +1 -1
- package/dist/__tests__/upgrade-runner.test.js +12 -0
- package/dist/__tests__/upgrade-runner.test.js.map +1 -1
- package/dist/__tests__/upgrade.test.d.ts +2 -0
- package/dist/__tests__/upgrade.test.d.ts.map +1 -0
- package/dist/__tests__/upgrade.test.js +56 -0
- package/dist/__tests__/upgrade.test.js.map +1 -0
- package/dist/__tests__/vocabulary-leak.test.d.ts +27 -13
- package/dist/__tests__/vocabulary-leak.test.d.ts.map +1 -1
- package/dist/__tests__/vocabulary-leak.test.js +169 -14
- package/dist/__tests__/vocabulary-leak.test.js.map +1 -1
- package/dist/commands/__tests__/setup-detect-surfaces.test.d.ts +15 -0
- package/dist/commands/__tests__/setup-detect-surfaces.test.d.ts.map +1 -0
- package/dist/commands/__tests__/setup-detect-surfaces.test.js +149 -0
- package/dist/commands/__tests__/setup-detect-surfaces.test.js.map +1 -0
- package/dist/commands/admin/seedRegistryEntries.generated.js +2 -2
- package/dist/commands/admin/seedRegistryEntries.generated.js.map +1 -1
- package/dist/commands/admin/seedRegistryEntries.test.js +1 -1
- package/dist/commands/admin/seedRegistryEntries.test.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +30 -3
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/authority-domains.d.ts.map +1 -1
- package/dist/commands/authority-domains.js +2 -1
- package/dist/commands/authority-domains.js.map +1 -1
- package/dist/commands/capture.d.ts +2 -0
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +5 -1
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/connect-config.test.d.ts +2 -0
- package/dist/commands/connect-config.test.d.ts.map +1 -0
- package/dist/commands/connect-config.test.js +44 -0
- package/dist/commands/connect-config.test.js.map +1 -0
- package/dist/commands/connect-context.d.ts +45 -0
- package/dist/commands/connect-context.d.ts.map +1 -0
- package/dist/commands/connect-context.js +64 -0
- package/dist/commands/connect-context.js.map +1 -0
- package/dist/commands/connect-context.test.d.ts +2 -0
- package/dist/commands/connect-context.test.d.ts.map +1 -0
- package/dist/commands/connect-context.test.js +110 -0
- package/dist/commands/connect-context.test.js.map +1 -0
- package/dist/commands/connect-screens.d.ts +5 -6
- package/dist/commands/connect-screens.d.ts.map +1 -1
- package/dist/commands/connect-screens.js +28 -35
- package/dist/commands/connect-screens.js.map +1 -1
- package/dist/commands/connect.d.ts +16 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +21 -20
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +19 -0
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/handshake.d.ts +59 -4
- package/dist/commands/handshake.d.ts.map +1 -1
- package/dist/commands/handshake.js +392 -97
- package/dist/commands/handshake.js.map +1 -1
- package/dist/commands/method.d.ts.map +1 -1
- package/dist/commands/method.js +2 -1
- package/dist/commands/method.js.map +1 -1
- package/dist/commands/orient.d.ts +48 -0
- package/dist/commands/orient.d.ts.map +1 -1
- package/dist/commands/orient.js +25 -5
- package/dist/commands/orient.js.map +1 -1
- package/dist/commands/profile.d.ts +1 -14
- package/dist/commands/profile.d.ts.map +1 -1
- package/dist/commands/profile.js +89 -72
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/proof-run.d.ts +51 -0
- package/dist/commands/proof-run.d.ts.map +1 -0
- package/dist/commands/proof-run.js +209 -0
- package/dist/commands/proof-run.js.map +1 -0
- package/dist/commands/reject.d.ts.map +1 -1
- package/dist/commands/reject.js +2 -1
- package/dist/commands/reject.js.map +1 -1
- package/dist/commands/relate.d.ts.map +1 -1
- package/dist/commands/relate.js +4 -2
- package/dist/commands/relate.js.map +1 -1
- package/dist/commands/session.d.ts +26 -2
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +216 -31
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/setup-audit.d.ts +59 -0
- package/dist/commands/setup-audit.d.ts.map +1 -0
- package/dist/commands/setup-audit.js +250 -0
- package/dist/commands/setup-audit.js.map +1 -0
- package/dist/commands/setup-detect-surfaces.d.ts.map +1 -1
- package/dist/commands/setup-detect-surfaces.js +10 -1
- package/dist/commands/setup-detect-surfaces.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +2 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +31 -10
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +2 -1
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/whoami.d.ts +12 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +70 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/commands/whoami.test.d.ts +2 -0
- package/dist/commands/whoami.test.d.ts.map +1 -0
- package/dist/commands/whoami.test.js +50 -0
- package/dist/commands/whoami.test.js.map +1 -0
- package/dist/formatters/__tests__/orient-provenance.test.d.ts +7 -0
- package/dist/formatters/__tests__/orient-provenance.test.d.ts.map +1 -0
- package/dist/formatters/__tests__/orient-provenance.test.js +454 -0
- package/dist/formatters/__tests__/orient-provenance.test.js.map +1 -0
- package/dist/formatters/audit.d.ts +6 -0
- package/dist/formatters/audit.d.ts.map +1 -1
- package/dist/formatters/audit.js.map +1 -1
- package/dist/formatters/entry.d.ts +21 -0
- package/dist/formatters/entry.d.ts.map +1 -1
- package/dist/formatters/entry.js +46 -5
- package/dist/formatters/entry.js.map +1 -1
- package/dist/formatters/handshake.d.ts +7 -3
- package/dist/formatters/handshake.d.ts.map +1 -1
- package/dist/formatters/handshake.js +26 -23
- package/dist/formatters/handshake.js.map +1 -1
- package/dist/formatters/orient.d.ts +54 -0
- package/dist/formatters/orient.d.ts.map +1 -1
- package/dist/formatters/orient.js +71 -2
- package/dist/formatters/orient.js.map +1 -1
- package/dist/generators/adapters.js +4 -4
- package/dist/generators/region-projections.d.ts +18 -0
- package/dist/generators/region-projections.d.ts.map +1 -0
- package/dist/generators/region-projections.js +49 -0
- package/dist/generators/region-projections.js.map +1 -0
- package/dist/generators/region-projections.test.d.ts +2 -0
- package/dist/generators/region-projections.test.d.ts.map +1 -0
- package/dist/generators/region-projections.test.js +63 -0
- package/dist/generators/region-projections.test.js.map +1 -0
- package/dist/generators/region.d.ts +24 -0
- package/dist/generators/region.d.ts.map +1 -0
- package/dist/generators/region.js +87 -0
- package/dist/generators/region.js.map +1 -0
- package/dist/generators/region.test.d.ts +2 -0
- package/dist/generators/region.test.d.ts.map +1 -0
- package/dist/generators/region.test.js +126 -0
- package/dist/generators/region.test.js.map +1 -0
- package/dist/index.js +111 -9
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +101 -4
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +225 -11
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/notice-marker.d.ts +3 -0
- package/dist/lib/notice-marker.d.ts.map +1 -0
- package/dist/lib/notice-marker.js +53 -0
- package/dist/lib/notice-marker.js.map +1 -0
- package/dist/lib/onboarding-path-b.d.ts.map +1 -1
- package/dist/lib/onboarding-path-b.js +0 -1
- package/dist/lib/onboarding-path-b.js.map +1 -1
- package/dist/lib/onboarding-shared.d.ts +0 -1
- package/dist/lib/onboarding-shared.d.ts.map +1 -1
- package/dist/lib/onboarding-shared.js +0 -16
- package/dist/lib/onboarding-shared.js.map +1 -1
- package/dist/lib/profiles.d.ts +3 -1
- package/dist/lib/profiles.d.ts.map +1 -1
- package/dist/lib/profiles.js +9 -6
- package/dist/lib/profiles.js.map +1 -1
- package/dist/lib/session.d.ts +10 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/session.js +26 -1
- package/dist/lib/session.js.map +1 -1
- package/dist/lib/tokenConstants.d.ts +2 -0
- package/dist/lib/tokenConstants.d.ts.map +1 -1
- package/dist/lib/tokenConstants.js +2 -0
- package/dist/lib/tokenConstants.js.map +1 -1
- package/dist/lib/update-check.d.ts +26 -11
- package/dist/lib/update-check.d.ts.map +1 -1
- package/dist/lib/update-check.js +123 -73
- package/dist/lib/update-check.js.map +1 -1
- package/dist/lib/upgrade-runner.d.ts +2 -1
- package/dist/lib/upgrade-runner.d.ts.map +1 -1
- package/dist/lib/upgrade-runner.js +8 -7
- package/dist/lib/upgrade-runner.js.map +1 -1
- package/dist/setup/perimeter.d.ts +10 -0
- package/dist/setup/perimeter.d.ts.map +1 -1
- package/dist/setup/perimeter.js +21 -6
- package/dist/setup/perimeter.js.map +1 -1
- package/package.json +2 -1
- package/dist/__tests__/setup.test.d.ts +0 -2
- package/dist/__tests__/setup.test.d.ts.map +0 -1
- package/dist/__tests__/setup.test.js +0 -141
- package/dist/__tests__/setup.test.js.map +0 -1
- package/dist/generators/__tests__/surface-profiles.test.d.ts +0 -2
- package/dist/generators/__tests__/surface-profiles.test.d.ts.map +0 -1
- package/dist/generators/__tests__/surface-profiles.test.js +0 -89
- package/dist/generators/__tests__/surface-profiles.test.js.map +0 -1
- package/dist/lib/onboarding-phases.d.ts +0 -9
- package/dist/lib/onboarding-phases.d.ts.map +0 -1
- package/dist/lib/onboarding-phases.js +0 -120
- package/dist/lib/onboarding-phases.js.map +0 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* pb handshake — generate context files for AI developer tools.
|
|
3
3
|
* Context export wiring (read-only filesystem bridge; GLO-63, DEC-161) — not a product surface.
|
|
4
4
|
*/
|
|
5
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync, appendFileSync, unlinkSync, statSync } from 'fs';
|
|
6
|
-
import { join, dirname, resolve, basename } from 'path';
|
|
5
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync, appendFileSync, unlinkSync, statSync, renameSync, rmSync, realpathSync } from 'fs';
|
|
6
|
+
import { join, dirname, resolve, basename, relative, sep, isAbsolute } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { createHash } from 'crypto';
|
|
@@ -21,6 +21,8 @@ import { generateChainRules } from '../generators/chain-rules.js';
|
|
|
21
21
|
import { saveHandshakeState, loadPreviousState, diffHandshakeState, formatDiff, buildCurrentState, } from '../generators/handshake-diff.js';
|
|
22
22
|
import { resolveSurfaceProfile } from '../generators/surface-profiles.js';
|
|
23
23
|
import { formatHandshakeReport } from '../formatters/handshake.js';
|
|
24
|
+
import { classifyAdapterFile, detectEol, spliceAppend, spliceReplace } from '../generators/region.js';
|
|
25
|
+
import { REGION_PROJECTIONS } from '../generators/region-projections.js';
|
|
24
26
|
import { readManifest, readManifestStatus, filterByAdoptionState } from '../generators/manifest.js';
|
|
25
27
|
import { generateBoundaryManifest, getBoundaryEnforcementMode } from '../generators/boundary-manifest.js';
|
|
26
28
|
import { loadMethodRegistry } from '../lib/method-registry.js';
|
|
@@ -89,6 +91,71 @@ export function writeDormantMarkerToFile(filePath) {
|
|
|
89
91
|
appendFileSync(filePath, `\n${DORMANT_MARKER}\n`);
|
|
90
92
|
return 'written';
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* renameSurfaceForDormancy — rename a projected surface file to `<path>.dormant`
|
|
96
|
+
* so external scanners (which glob *.md / *.mdc) stop loading it. WP-426 E4.
|
|
97
|
+
* The HTML-comment marker is invisible to scanners; the extension change removes
|
|
98
|
+
* the file from their glob set. Only touches files carrying the auto-gen MARKER.
|
|
99
|
+
* Runs on an INDEPENDENT existence check (not gated on marker-write result) so
|
|
100
|
+
* legacy WP-379 marker-only files still rename. Idempotent.
|
|
101
|
+
* Codex P1: when a .dormant already exists, only replace it if its body matches the
|
|
102
|
+
* live file. A divergent .dormant (user edited it) is preserved and reported as 'drift'
|
|
103
|
+
* rather than force-deleted — no silent data loss.
|
|
104
|
+
* @returns 'renamed' | 'replaced' | 'already-dormant' | 'skipped' | 'drift'
|
|
105
|
+
*/
|
|
106
|
+
export function renameSurfaceForDormancy(filePath) {
|
|
107
|
+
const dormantPath = `${filePath}.dormant`;
|
|
108
|
+
if (!existsSync(filePath))
|
|
109
|
+
return existsSync(dormantPath) ? 'already-dormant' : 'skipped';
|
|
110
|
+
if (!readFileSync(filePath, 'utf8').includes(MARKER))
|
|
111
|
+
return 'skipped';
|
|
112
|
+
const replacing = existsSync(dormantPath);
|
|
113
|
+
if (replacing) {
|
|
114
|
+
// Codex P1: the existing .dormant may carry user edits (e.g. the user reactivated to
|
|
115
|
+
// the live path but kept an edited dormant sibling). Compare normalized bodies
|
|
116
|
+
// (DORMANT_MARKER + volatile auto-gen timestamp stripped from both); if they diverge,
|
|
117
|
+
// preserve the .dormant and signal drift instead of force-replacing it.
|
|
118
|
+
const strip = (s) => normalizeHandshakeContentForComparison(s.split(DORMANT_MARKER).join('')).trimEnd();
|
|
119
|
+
if (strip(readFileSync(filePath, 'utf8')) !== strip(readFileSync(dormantPath, 'utf8'))) {
|
|
120
|
+
return 'drift';
|
|
121
|
+
}
|
|
122
|
+
rmSync(dormantPath, { force: true });
|
|
123
|
+
}
|
|
124
|
+
renameSync(filePath, dormantPath);
|
|
125
|
+
return replacing ? 'replaced' : 'renamed';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* restoreSurfaceFromDormant — clean the orphan `<path>.dormant` sibling after a
|
|
129
|
+
* raised (observe→project) asset has been re-projected fresh from the DB body by
|
|
130
|
+
* the normal write loop. WP-426 E4.
|
|
131
|
+
* Normalizes the volatile auto-gen timestamp (via normalizeHandshakeContentForComparison)
|
|
132
|
+
* before comparing, else the lowering-time timestamp would always force a false
|
|
133
|
+
* 'orphan-drift'. Identical body → remove. Differs (user edited) → preserve + drift.
|
|
134
|
+
* @returns 'restored' | 'orphan-drift' | 'skipped'
|
|
135
|
+
*/
|
|
136
|
+
export function restoreSurfaceFromDormant(filePath) {
|
|
137
|
+
const dormantPath = `${filePath}.dormant`;
|
|
138
|
+
if (!existsSync(dormantPath))
|
|
139
|
+
return 'skipped';
|
|
140
|
+
// WP-426 E4: no fresh live projection this run (surface filtered / user-owned / not written) →
|
|
141
|
+
// nothing to reconcile; absence of fresh is NOT evidence of a user edit. Leave the .dormant.
|
|
142
|
+
if (!existsSync(filePath))
|
|
143
|
+
return 'skipped';
|
|
144
|
+
const fresh = readFileSync(filePath, 'utf8');
|
|
145
|
+
// Codex P2: only reconcile against a PB-managed projection. A live file WITHOUT the auto-gen
|
|
146
|
+
// MARKER is a user-owned file (shouldWriteAdapter would have skipped reprojecting it),
|
|
147
|
+
// so it was NOT raised this run — comparing the .dormant against it would wrongly delete it or
|
|
148
|
+
// re-fire "edited while dormant" drift. Leave the .dormant untouched.
|
|
149
|
+
if (!fresh.includes(MARKER))
|
|
150
|
+
return 'skipped';
|
|
151
|
+
const dormant = readFileSync(dormantPath, 'utf8').split(DORMANT_MARKER).join('');
|
|
152
|
+
const norm = (s) => normalizeHandshakeContentForComparison(s).trimEnd();
|
|
153
|
+
if (norm(dormant) === norm(fresh)) {
|
|
154
|
+
rmSync(dormantPath, { force: true });
|
|
155
|
+
return 'restored';
|
|
156
|
+
}
|
|
157
|
+
return 'orphan-drift';
|
|
158
|
+
}
|
|
92
159
|
/**
|
|
93
160
|
* deriveDormantFilePaths — compute the set of on-disk file paths that would have
|
|
94
161
|
* been projected for a given dormant asset (by name and assetKind).
|
|
@@ -100,7 +167,8 @@ export function writeDormantMarkerToFile(filePath) {
|
|
|
100
167
|
*
|
|
101
168
|
* @param asset The dormant asset from the server.
|
|
102
169
|
* @param cwd Current working directory (project root).
|
|
103
|
-
* @returns
|
|
170
|
+
* @returns Each candidate path paired with its owning surface, so callers can
|
|
171
|
+
* filter by the run's allowedTargets (--surfaces) and the perimeter.
|
|
104
172
|
*/
|
|
105
173
|
function deriveDormantFilePaths(asset, cwd) {
|
|
106
174
|
// Defense-in-depth: even though `name` originates from platform-seeded DB
|
|
@@ -110,26 +178,22 @@ function deriveDormantFilePaths(asset, cwd) {
|
|
|
110
178
|
if (!/^[A-Za-z0-9 ._-]+$/.test(asset.name)) {
|
|
111
179
|
return [];
|
|
112
180
|
}
|
|
113
|
-
const
|
|
181
|
+
const out = [];
|
|
114
182
|
const { name, assetKind } = asset;
|
|
115
183
|
if (assetKind === 'skill') {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Codex skill
|
|
119
|
-
paths.push(join(cwd, '.codex', 'skills', `${name}.md`));
|
|
184
|
+
out.push({ path: join(cwd, '.cursor', 'skills', name, 'SKILL.md'), surface: 'cursor' });
|
|
185
|
+
out.push({ path: join(cwd, '.codex', 'skills', `${name}.md`), surface: 'codex' });
|
|
120
186
|
}
|
|
121
187
|
else if (assetKind === 'rule' || assetKind === 'hook') {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Claude rule
|
|
125
|
-
paths.push(join(cwd, '.claude', 'rules', `${name}.md`));
|
|
188
|
+
out.push({ path: join(cwd, '.cursor', 'rules', `${name}.mdc`), surface: 'cursor' });
|
|
189
|
+
out.push({ path: join(cwd, '.claude', 'rules', `${name}.md`), surface: 'claude' });
|
|
126
190
|
}
|
|
127
|
-
return
|
|
191
|
+
return out;
|
|
128
192
|
}
|
|
129
193
|
/**
|
|
130
194
|
* Single-shot health probe — calls `workspace.health` and inspects
|
|
131
195
|
* `starterSetupSeeded`. Does NOT poll internally; polling is the caller's
|
|
132
|
-
* responsibility (connect-
|
|
196
|
+
* responsibility (connect-context.ts).
|
|
133
197
|
*
|
|
134
198
|
* Returns:
|
|
135
199
|
* - `seeds-ready` — health query succeeded AND starterSetupSeeded is true
|
|
@@ -165,7 +229,7 @@ export async function probeStarterSetupSeeded() {
|
|
|
165
229
|
* Poll `probeStarterSetupSeeded` up to MAX_POLLS times (10s at 500ms intervals).
|
|
166
230
|
* Returns the final probe result — caller decides how to render the outcome.
|
|
167
231
|
*
|
|
168
|
-
* Exported so connect-
|
|
232
|
+
* Exported so connect-context.ts can use it without re-implementing the loop.
|
|
169
233
|
*/
|
|
170
234
|
export async function pollUntilSeedsReady() {
|
|
171
235
|
for (let poll = 0; poll < MAX_POLLS; poll++) {
|
|
@@ -557,6 +621,28 @@ function setupAuthoringPath(cwd, asset) {
|
|
|
557
621
|
const dir = setupAuthoringDirForKind(asset.assetKind);
|
|
558
622
|
if (!dir)
|
|
559
623
|
return null;
|
|
624
|
+
// WP-426 E3: recorded authoring source path wins so reprojection lands where the
|
|
625
|
+
// user authored — no duplicate (TEN-1920). Stored relative to .productbrain/.
|
|
626
|
+
if (asset.authoringPath && asset.authoringPath.trim()) {
|
|
627
|
+
// Codex P1/P2: authoringPath comes from the DB and is untrusted at projection
|
|
628
|
+
// time. It is probed via existsSync/readFileSync (and later writeFileSync) in the
|
|
629
|
+
// writeback loop BEFORE any assertSetupWritePath guard runs. Containment alone is
|
|
630
|
+
// not enough — a bad DB row could still point at a directory (e.g. "skills") or an
|
|
631
|
+
// internal PB file (".authoring-sync.json", "manifest.yaml"), which the loop would
|
|
632
|
+
// mis-handle as markdown (throw on a directory read, or overwrite PB state).
|
|
633
|
+
// Constrain to a kind-appropriate markdown file (<dir>/**/*.md); otherwise fall
|
|
634
|
+
// back to the safe name-derived path.
|
|
635
|
+
const pbDir = join(cwd, '.productbrain');
|
|
636
|
+
const candidate = resolve(pbDir, asset.authoringPath);
|
|
637
|
+
const within = relative(pbDir, candidate);
|
|
638
|
+
const withinPosix = within.split(sep).join('/');
|
|
639
|
+
if (!within.startsWith('..') &&
|
|
640
|
+
!isAbsolute(within) &&
|
|
641
|
+
withinPosix.startsWith(`${dir}/`) &&
|
|
642
|
+
withinPosix.toLowerCase().endsWith('.md')) {
|
|
643
|
+
return candidate;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
560
646
|
return join(cwd, '.productbrain', dir, setupAuthoringFilename(asset.name, asset.entryId));
|
|
561
647
|
}
|
|
562
648
|
function renderSetupAuthoringFile(asset) {
|
|
@@ -588,7 +674,17 @@ function loadAuthoringSyncState(productbrainDir) {
|
|
|
588
674
|
if (parsed.version !== 1 || !parsed.assets || typeof parsed.assets !== 'object') {
|
|
589
675
|
return { version: 1, assets: {} };
|
|
590
676
|
}
|
|
591
|
-
|
|
677
|
+
// Codex P2: the dormant registries are consumed via .includes() in the dormant loop
|
|
678
|
+
// BEFORE its per-file try/catch, so a malformed .authoring-sync.json (a field set to
|
|
679
|
+
// an object/number instead of an array) would throw a TypeError and abort the whole
|
|
680
|
+
// handshake. Coerce to a string[] (mirrors the assets validation above) to fail open.
|
|
681
|
+
const toStringArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
|
|
682
|
+
return {
|
|
683
|
+
version: 1,
|
|
684
|
+
assets: parsed.assets,
|
|
685
|
+
dormantRenamed: toStringArray(parsed.dormantRenamed),
|
|
686
|
+
dormantReactivated: toStringArray(parsed.dormantReactivated),
|
|
687
|
+
};
|
|
592
688
|
}
|
|
593
689
|
catch {
|
|
594
690
|
return { version: 1, assets: {} };
|
|
@@ -624,7 +720,7 @@ export function classifyDriftBucket(filePath) {
|
|
|
624
720
|
return { bucket: 'user-owned', expectedHash: '', actualHash: '' };
|
|
625
721
|
}
|
|
626
722
|
// Marker present but no hash trailer → legacy / pre-S0c projection: treat as
|
|
627
|
-
// clean (the hash trailer was added in WP-345 S0c). The
|
|
723
|
+
// clean (the hash trailer was added in WP-345 S0c). The user-owned-vs-clean
|
|
628
724
|
// semantic falls back to existing shouldWriteAdapter behavior.
|
|
629
725
|
const trailerMatch = content.match(DRIFT_HASH_TRAILER_REGEX);
|
|
630
726
|
if (!trailerMatch) {
|
|
@@ -664,7 +760,7 @@ function deduplicateEntries(entries) {
|
|
|
664
760
|
* the auto-gen MARKER whose lowercase-normalized filename does NOT match any
|
|
665
761
|
* current active-asset materializedFilename, the file is unlinked.
|
|
666
762
|
*
|
|
667
|
-
* User-
|
|
763
|
+
* User-owned files (no MARKER) are never touched, regardless of name.
|
|
668
764
|
*
|
|
669
765
|
* Linux case-collision disambiguation:
|
|
670
766
|
* 1. Exact match (lowercase name == any canonical name): survives.
|
|
@@ -722,7 +818,7 @@ export function resolveProjectionCollision(cwd, assetNames, log, logErr) {
|
|
|
722
818
|
continue; // unreadable file — skip
|
|
723
819
|
}
|
|
724
820
|
if (!content.includes(MARKER))
|
|
725
|
-
continue; // user-
|
|
821
|
+
continue; // user-owned — never touch
|
|
726
822
|
const stem = basename(filename, ext);
|
|
727
823
|
const normalizedStem = normalizeMaterializedFilename(stem);
|
|
728
824
|
const group = groups.get(normalizedStem) ?? [];
|
|
@@ -815,9 +911,9 @@ export function resolveProjectionCollision(cwd, assetNames, log, logErr) {
|
|
|
815
911
|
return { results, collisionTens };
|
|
816
912
|
}
|
|
817
913
|
export async function runHandshake(options = {}) {
|
|
818
|
-
const config = await getConfigOrGuide(() => runHandshake(options));
|
|
914
|
+
const config = await getConfigOrGuide(async () => { await runHandshake(options); });
|
|
819
915
|
if (!config)
|
|
820
|
-
return;
|
|
916
|
+
return undefined;
|
|
821
917
|
const cwd = process.cwd();
|
|
822
918
|
const force = options.force ?? false;
|
|
823
919
|
const dryRun = options.dryRun ?? false;
|
|
@@ -944,9 +1040,6 @@ export async function runHandshake(options = {}) {
|
|
|
944
1040
|
// Hoisted here (same scope as dbAssetRows) so both the skills/rules projection loop
|
|
945
1041
|
// AND the authoring-file projection loop can skip failed assets.
|
|
946
1042
|
const bodyFetchFailedEntryIds = new Set();
|
|
947
|
-
// WP-379 S5b: whether any setup_receipt exists for this workspace (first-run UX gate).
|
|
948
|
-
// undefined when server is pre-S5b (treat as unknown → suppress drift TENs conservatively).
|
|
949
|
-
let hasAnyReceipt = undefined;
|
|
950
1043
|
const dbProjectionHashes = new Map();
|
|
951
1044
|
const syncDriftTensToFire = [];
|
|
952
1045
|
const deferredAuthoringBaselineEntryIds = new Set();
|
|
@@ -995,6 +1088,7 @@ export async function runHandshake(options = {}) {
|
|
|
995
1088
|
assetKind: item.assetKind,
|
|
996
1089
|
triggers: item.triggers,
|
|
997
1090
|
semanticRefs: item.semanticRefs,
|
|
1091
|
+
authoringPath: relative(pbDir, item.filePath).split(sep).join('/'),
|
|
998
1092
|
});
|
|
999
1093
|
return { filePath: item.filePath, ...result };
|
|
1000
1094
|
}));
|
|
@@ -1034,14 +1128,10 @@ export async function runHandshake(options = {}) {
|
|
|
1034
1128
|
// Pre-S4 server — treat entire response as active assets with no dormant list.
|
|
1035
1129
|
dbAssets = rawResponse;
|
|
1036
1130
|
dormantDbAssetRows = [];
|
|
1037
|
-
hasAnyReceipt = undefined; // unknown on legacy servers
|
|
1038
1131
|
}
|
|
1039
1132
|
else {
|
|
1040
1133
|
dbAssets = rawResponse.activeAssets ?? [];
|
|
1041
1134
|
dormantDbAssetRows = rawResponse.dormantAssets ?? [];
|
|
1042
|
-
// WP-379 S5b: extract hasAnyReceipt when provided by the server.
|
|
1043
|
-
// undefined means pre-S5b server — we treat as unknown (no receipts assumed).
|
|
1044
|
-
hasAnyReceipt = rawResponse.hasAnyReceipt;
|
|
1045
1135
|
}
|
|
1046
1136
|
}
|
|
1047
1137
|
if (dbAssets.length > 0) {
|
|
@@ -1073,7 +1163,19 @@ export async function runHandshake(options = {}) {
|
|
|
1073
1163
|
}
|
|
1074
1164
|
}
|
|
1075
1165
|
if (bodyFetchFailedEntryIds.size > 0) {
|
|
1076
|
-
|
|
1166
|
+
// WP-439 S4: strict-by-default. Any body-fetch failure is a hard
|
|
1167
|
+
// failure unless --lenient is passed. The strict path surfaces an
|
|
1168
|
+
// actionable pointer at the audit/repair command operators can run
|
|
1169
|
+
// to diagnose and fix orphaned setup-asset rows.
|
|
1170
|
+
const failedList = [...bodyFetchFailedEntryIds].join(', ');
|
|
1171
|
+
if (options.lenient) {
|
|
1172
|
+
logErr(`Warning: ${bodyFetchFailedEntryIds.size} asset(s) skipped due to body fetch failure (--lenient): ${failedList}`);
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
throw new CLIError(`${bodyFetchFailedEntryIds.size} asset(s) failed body fetch: ${failedList}. ` +
|
|
1176
|
+
`Run \`pb setup-audit\` to diagnose, then \`pb setup-audit --repair\` to reseed. ` +
|
|
1177
|
+
`Pass --lenient to suppress this and continue with affected entries skipped (legacy behaviour).`, { code: ErrorCode.INTERNAL, category: 'internal' });
|
|
1178
|
+
}
|
|
1077
1179
|
}
|
|
1078
1180
|
}
|
|
1079
1181
|
// Map DB assets to CanonicalSkill/CanonicalRule shapes
|
|
@@ -1406,7 +1508,9 @@ export async function runHandshake(options = {}) {
|
|
|
1406
1508
|
const authoringPath = setupAuthoringPath(cwd, asset);
|
|
1407
1509
|
if (!authoringPath)
|
|
1408
1510
|
continue;
|
|
1409
|
-
|
|
1511
|
+
// Review: use relative() for correct cross-platform path computation (string-replace
|
|
1512
|
+
// mis-fires when cwd uses a non-'/' separator), matching the dormant passes below.
|
|
1513
|
+
const relativeAuthoringPath = relative(cwd, authoringPath);
|
|
1410
1514
|
const bodyHash = setupAuthoringAssetHash(asset);
|
|
1411
1515
|
const authoringExists = existsSync(authoringPath);
|
|
1412
1516
|
const tracked = authoringSyncState.assets[asset.entryId];
|
|
@@ -1506,8 +1610,8 @@ export async function runHandshake(options = {}) {
|
|
|
1506
1610
|
const writes = [
|
|
1507
1611
|
...(contextContent ? [{ path: join(cwd, '.productbrain', 'context.md'), relative: '.productbrain/context.md', content: contextContent, dirs: join(cwd, '.productbrain'), isAdapter: false }] : []),
|
|
1508
1612
|
{ path: join(cwd, '.productbrain', 'briefing.md'), relative: '.productbrain/briefing.md', content: briefingContent, isAdapter: false },
|
|
1509
|
-
{ path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true, target: 'codex' },
|
|
1510
|
-
{ path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true, target: 'claude' },
|
|
1613
|
+
{ path: join(cwd, 'AGENTS.md'), relative: 'AGENTS.md', content: agentsContent, isAdapter: true, target: 'codex', augmentTarget: true },
|
|
1614
|
+
{ path: join(cwd, 'CLAUDE.md'), relative: 'CLAUDE.md', content: claudeContent, isAdapter: true, target: 'claude', augmentTarget: true },
|
|
1511
1615
|
{ path: join(cwd, '.cursor', 'rules', 'chain.mdc'), relative: '.cursor/rules/chain.mdc', content: cursorContent, dirs: join(cwd, '.cursor', 'rules'), isAdapter: true, target: 'cursor' },
|
|
1512
1616
|
{ path: join(cwd, '.github', 'copilot-instructions.md'), relative: '.github/copilot-instructions.md', content: copilotContent, dirs: join(cwd, '.github'), isAdapter: true, target: 'copilot' },
|
|
1513
1617
|
...(boundaryManifestContent ? [{
|
|
@@ -1638,7 +1742,7 @@ export async function runHandshake(options = {}) {
|
|
|
1638
1742
|
return null; // fail-open
|
|
1639
1743
|
}
|
|
1640
1744
|
});
|
|
1641
|
-
const
|
|
1745
|
+
const userOwnedSkipped = [];
|
|
1642
1746
|
const projectedHashUpdates = new Map();
|
|
1643
1747
|
const cleanBucketPaths = [];
|
|
1644
1748
|
const tamperedBucket = [];
|
|
@@ -1651,6 +1755,29 @@ export async function runHandshake(options = {}) {
|
|
|
1651
1755
|
if (projection)
|
|
1652
1756
|
projectedHashUpdates.set(entryId, projection.hash);
|
|
1653
1757
|
};
|
|
1758
|
+
// TEN-2155: region augmentation context + symlink dedupe.
|
|
1759
|
+
// codexActive gates the AGENTS.md skills-index pointer (region-projections.ts).
|
|
1760
|
+
const regionCtx = { codexActive: allowedTargets.has('codex') };
|
|
1761
|
+
const malformedRegionPaths = [];
|
|
1762
|
+
// If two augment targets resolve to the same inode (e.g. CLAUDE.md symlinked to AGENTS.md),
|
|
1763
|
+
// augment only the first-seen (AGENTS.md precedes CLAUDE.md in `writes`) to avoid double-injection.
|
|
1764
|
+
const seenAugmentRealpaths = new Set();
|
|
1765
|
+
const augmentTargetSkip = new Set();
|
|
1766
|
+
for (const w of writes) {
|
|
1767
|
+
if (!w.augmentTarget || !w.target || !allowedTargets.has(w.target) || !existsSync(w.path))
|
|
1768
|
+
continue;
|
|
1769
|
+
let real;
|
|
1770
|
+
try {
|
|
1771
|
+
real = realpathSync(w.path);
|
|
1772
|
+
}
|
|
1773
|
+
catch {
|
|
1774
|
+
real = w.path;
|
|
1775
|
+
}
|
|
1776
|
+
if (seenAugmentRealpaths.has(real))
|
|
1777
|
+
augmentTargetSkip.add(w.path);
|
|
1778
|
+
else
|
|
1779
|
+
seenAugmentRealpaths.add(real);
|
|
1780
|
+
}
|
|
1654
1781
|
for (const w of writes) {
|
|
1655
1782
|
// Surface filtering: skip adapter writes for targets not in the allowed set
|
|
1656
1783
|
if (w.target && !allowedTargets.has(w.target)) {
|
|
@@ -1659,13 +1786,78 @@ export async function runHandshake(options = {}) {
|
|
|
1659
1786
|
previewPlan.push({ path: w.relative, status: 'filtered' });
|
|
1660
1787
|
continue;
|
|
1661
1788
|
}
|
|
1789
|
+
// ── TEN-2155: augment user-owned CLAUDE.md / AGENTS.md with a marked PB region ──
|
|
1790
|
+
// Runs ahead of the legacy MARKER-keyed gates. Only existing augmentable/region-present
|
|
1791
|
+
// files are spliced here; absent + legacy-pb-managed fall through to the legacy path.
|
|
1792
|
+
if (w.augmentTarget) {
|
|
1793
|
+
const projection = REGION_PROJECTIONS[w.target];
|
|
1794
|
+
if (projection && existsSync(w.path)) {
|
|
1795
|
+
if (augmentTargetSkip.has(w.path)) {
|
|
1796
|
+
filesSkipped.push({ path: w.relative, reason: 'symlinked to another augment target — augmented once' });
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const disk = readFileSync(w.path, 'utf8');
|
|
1800
|
+
const cls = classifyAdapterFile(disk);
|
|
1801
|
+
if (cls === 'augmentable' || cls === 'region-present') {
|
|
1802
|
+
const eol = detectEol(disk);
|
|
1803
|
+
const region = replaceVocabTokens(projection.build(regionCtx, eol), handshakeVocabCtx);
|
|
1804
|
+
// Defense-in-depth: the composed region (post vocab-token resolution) must itself be a
|
|
1805
|
+
// single well-formed region. v1 content has no vocab tokens so this is a no-op, but it
|
|
1806
|
+
// guards the documented future override/vocab seam from injecting a stray sentinel/MARKER.
|
|
1807
|
+
if (classifyAdapterFile(region) !== 'region-present') {
|
|
1808
|
+
filesSkipped.push({ path: w.relative, reason: 'internal: composed PB region malformed — skipped to protect your file' });
|
|
1809
|
+
if (preview)
|
|
1810
|
+
previewPlan.push({ path: w.relative, status: 'needs-attention' });
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
const candidate = cls === 'augmentable' ? spliceAppend(disk, region, eol) : spliceReplace(disk, region);
|
|
1814
|
+
if (candidate === disk) {
|
|
1815
|
+
filesSkipped.push({ path: w.relative, reason: 'unchanged' });
|
|
1816
|
+
if (preview)
|
|
1817
|
+
previewPlan.push({ path: w.relative, status: 'unchanged' });
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
if (preview || dryRun) {
|
|
1821
|
+
filesWritten.push(w.relative + (dryRun ? ' (dry run)' : ''));
|
|
1822
|
+
if (preview)
|
|
1823
|
+
previewPlan.push({ path: w.relative, status: 'would-augment' });
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (authorityPreviewOnly) {
|
|
1827
|
+
// apply + materialize observe/off: honor authority — do NOT write; mirror the legacy
|
|
1828
|
+
// preview-only skip so the report does not falsely list it as written.
|
|
1829
|
+
filesSkipped.push({ path: w.relative, reason: `preview-only (materialize: ${manifestStatus.mode})` });
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
assertSetupWritePath(w.path, perimeterManifest);
|
|
1833
|
+
writeFileSync(w.path, candidate);
|
|
1834
|
+
filesWritten.push(w.relative);
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
if (cls === 'malformed') {
|
|
1838
|
+
filesSkipped.push({ path: w.relative, reason: 'malformed PB region — left untouched; fix the pb:region sentinels' });
|
|
1839
|
+
if (preview)
|
|
1840
|
+
previewPlan.push({ path: w.relative, status: 'needs-attention' });
|
|
1841
|
+
malformedRegionPaths.push(w.relative);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
// cls 'pb-managed' → fall through to the legacy whole-file re-projection path below.
|
|
1845
|
+
// cls 'opt-out' (file carries the pb:no-augment sentinel — e.g. this repo's committed
|
|
1846
|
+
// constitution) → fall through too, landing on the legacy user-owned skip: left untouched,
|
|
1847
|
+
// never spliced. This is how a file declines augmentation without losing its user-owned status.
|
|
1848
|
+
}
|
|
1849
|
+
// absent file → fall through to legacy whole-file CREATE.
|
|
1850
|
+
}
|
|
1662
1851
|
if (w.isAdapter && !shouldWriteAdapter(w.path, force)) {
|
|
1663
|
-
|
|
1852
|
+
// User-owned: a file at an adapter path without our auto-gen MARKER is the
|
|
1853
|
+
// user's own file. Leave it untouched — never overwrite, never treat as drift
|
|
1854
|
+
// or log a TEN (TEN-2150). Relayed to the connect screen via userOwnedSkipped.
|
|
1855
|
+
filesSkipped.push({ path: w.relative, reason: 'user-owned — left untouched (pb won\'t overwrite your file)' });
|
|
1664
1856
|
if (preview) {
|
|
1665
|
-
previewPlan.push({ path: w.relative, status: '
|
|
1857
|
+
previewPlan.push({ path: w.relative, status: 'user-owned' });
|
|
1666
1858
|
}
|
|
1667
1859
|
else {
|
|
1668
|
-
|
|
1860
|
+
userOwnedSkipped.push(w.relative);
|
|
1669
1861
|
}
|
|
1670
1862
|
continue;
|
|
1671
1863
|
}
|
|
@@ -1762,6 +1954,20 @@ export async function runHandshake(options = {}) {
|
|
|
1762
1954
|
}
|
|
1763
1955
|
});
|
|
1764
1956
|
}
|
|
1957
|
+
// Ordering note: this refusal runs AFTER the projected-hash flush so that files legitimately
|
|
1958
|
+
// written THIS run still record their hashes; malformed files were never written (no hash to record).
|
|
1959
|
+
// TEN-2155: in non-interactive apply, a malformed PB region is a refusal, not a silent skip.
|
|
1960
|
+
// Gated on applyMode (NOT writeMode): a malformed region is a user-file INTEGRITY fault, so the
|
|
1961
|
+
// refusal fires on any headless `--apply` regardless of materialize authority (STD-263 invariant vi —
|
|
1962
|
+
// "headless → non-zero refusal", unqualified). writeMode would suppress it under observe/off, where
|
|
1963
|
+
// the malformed file still gets enumerated but the corruption signal would be silently dropped.
|
|
1964
|
+
if (applyMode && malformedRegionPaths.length > 0 && (options.noPrompt || !process.stdout.isTTY)) {
|
|
1965
|
+
throw new CLIError(`Malformed PB region in: ${malformedRegionPaths.join(', ')}`, {
|
|
1966
|
+
code: ErrorCode.VALIDATION_FAILED,
|
|
1967
|
+
category: 'validation',
|
|
1968
|
+
guidance: 'Fix the `<!-- pb:region:start -->` / `<!-- pb:region:end -->` sentinels (one balanced pair), then re-run `pb handshake`.',
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1765
1971
|
// ── WP-421 S3: tampered-bucket resolution (doneWhen #17) ────────────────────
|
|
1766
1972
|
// Apply mode only. Tampered files were DEFERRED in the write loop above;
|
|
1767
1973
|
// here we either prompt the user (interactive TTY) or refuse (headless).
|
|
@@ -1836,7 +2042,7 @@ export async function runHandshake(options = {}) {
|
|
|
1836
2042
|
personalSkillCount: personalSkills.length > 0 ? personalSkills.length : undefined,
|
|
1837
2043
|
registrySource,
|
|
1838
2044
|
registryStale,
|
|
1839
|
-
|
|
2045
|
+
userOwnedSkipped: userOwnedSkipped.length > 0 ? userOwnedSkipped : undefined,
|
|
1840
2046
|
managedCleanCount: cleanBucketPaths.length || undefined,
|
|
1841
2047
|
tamperedFiles: refusedTamperedFiles,
|
|
1842
2048
|
}) + '\n');
|
|
@@ -1921,7 +2127,8 @@ export async function runHandshake(options = {}) {
|
|
|
1921
2127
|
await caller('setup.ingestSetupAsset', {
|
|
1922
2128
|
entryId: `SETUP-ADOPTED-${Date.now()}-${draftName.replace(/\s+/g, '-')}`,
|
|
1923
2129
|
name: draftName,
|
|
1924
|
-
|
|
2130
|
+
// WP-421 S3
|
|
2131
|
+
description: `Adopted from tampered projection at ${tamper.relative}.`,
|
|
1925
2132
|
body: tamperedContent,
|
|
1926
2133
|
assetKind: tamper.relative.includes('/skills/') ? 'skill' : 'rule',
|
|
1927
2134
|
triggers: [],
|
|
@@ -1940,79 +2147,162 @@ export async function runHandshake(options = {}) {
|
|
|
1940
2147
|
}
|
|
1941
2148
|
}
|
|
1942
2149
|
}
|
|
1943
|
-
// 8a. Dormant marker
|
|
1944
|
-
// For each dormant asset (gate-filtered by the server), locate any previously-projected
|
|
1945
|
-
// on-disk files and append the DORMANT_MARKER trailer. Files are NOT deleted.
|
|
1946
|
-
//
|
|
1947
|
-
// Drift TEN exclusion: dormant-marked files have the auto-gen MARKER, so
|
|
1948
|
-
// shouldWriteAdapter() returns true for them. However, because the asset is dormant,
|
|
1949
|
-
// it is NOT in the `writes` array — it was never queued for a fresh write. Therefore,
|
|
1950
|
-
// dormant files will never appear in forkedPaths (forkedPaths only catches files that
|
|
1951
|
-
// ARE in the writes array but fail shouldWriteAdapter). The dormant marker write is a
|
|
1952
|
-
// separate, independent pass that runs BEFORE the drift TEN check — intentionally
|
|
1953
|
-
// after the main write loop to avoid interfering with active asset writes.
|
|
1954
|
-
//
|
|
1955
|
-
// Fail-open: if a dormant marker write fails, log and continue. Never crash the handshake.
|
|
2150
|
+
// 8a. Dormant marker + .dormant rename (WP-379 S4 + WP-426 E4) — apply mode only.
|
|
1956
2151
|
const dormantMarkedPaths = [];
|
|
1957
2152
|
if (writeMode && dormantDbAssetRows.length > 0) {
|
|
2153
|
+
const dormantState = loadAuthoringSyncState(pbDir);
|
|
2154
|
+
let dormantStateChanged = false;
|
|
1958
2155
|
for (const dormantAsset of dormantDbAssetRows) {
|
|
1959
|
-
const
|
|
1960
|
-
|
|
2156
|
+
for (const { path: filePath, surface } of deriveDormantFilePaths(dormantAsset, cwd)) {
|
|
2157
|
+
// Codex P2: deriveDormantFilePaths emits every known surface path. Honor the run's
|
|
2158
|
+
// target set (allowedTargets = the --surfaces selection, else all manifest surfaces)
|
|
2159
|
+
// so a `--surfaces cursor` run never renames .claude/.codex to .dormant, and surfaces
|
|
2160
|
+
// outside the manifest are skipped silently (no false "could not dormant-mark" warning).
|
|
2161
|
+
if (!allowedTargets.has(surface))
|
|
2162
|
+
continue;
|
|
2163
|
+
// FIX 4: use relative() instead of string-replace for correct cross-platform behaviour.
|
|
2164
|
+
const rel = relative(cwd, filePath);
|
|
2165
|
+
const alreadyReactivated = dormantState.dormantReactivated?.includes(rel) ?? false;
|
|
2166
|
+
const previouslyRenamed = dormantState.dormantRenamed?.includes(rel) ?? false;
|
|
1961
2167
|
try {
|
|
1962
2168
|
assertSetupWritePath(filePath, perimeterManifest);
|
|
2169
|
+
// WP-426 E4: BUG 1 fix — hands-off set.
|
|
2170
|
+
//
|
|
2171
|
+
// Step 1: permanently hands-off — user already reactivated this path on a
|
|
2172
|
+
// prior run. Skip silently every time until they re-lower or raise in PB.
|
|
2173
|
+
// (Task 7 raise-cleanup must later also prune dormantReactivated for raised
|
|
2174
|
+
// assets — see the `dormantReactivated` comment on AuthoringSyncState.)
|
|
2175
|
+
if (alreadyReactivated) {
|
|
2176
|
+
continue; // leave file untouched; no TEN (already fired on first detection)
|
|
2177
|
+
}
|
|
2178
|
+
// Step 2: FIRST detection of manual reactivation — we previously renamed
|
|
2179
|
+
// this to .dormant but the user renamed it back to a live file. Push the
|
|
2180
|
+
// drift TEN exactly once, add to the permanent hands-off set, remove from
|
|
2181
|
+
// dormantRenamed, leave file untouched.
|
|
2182
|
+
if (previouslyRenamed && existsSync(filePath) && !existsSync(`${filePath}.dormant`)) {
|
|
2183
|
+
syncDriftTensToFire.push(`Dormant asset ${dormantAsset.entryId} was manually reactivated at ${rel}; left untouched. Re-lower or raise it in PB to resync.`);
|
|
2184
|
+
logErr(`Warning: ${rel} was manually un-dormanted; leaving it in place (drift).`);
|
|
2185
|
+
dormantState.dormantReactivated = [...(dormantState.dormantReactivated ?? []), rel];
|
|
2186
|
+
dormantState.dormantRenamed = (dormantState.dormantRenamed ?? []).filter((p) => p !== rel);
|
|
2187
|
+
dormantStateChanged = true;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
// Step 3: normal lowering — write dormant marker + rename to .dormant.
|
|
2191
|
+
// WP-426 E4 spec: every rename TARGET must also pass the perimeter guard.
|
|
2192
|
+
// Check the post-rename .dormant path BEFORE any FS mutation, so a guard
|
|
2193
|
+
// failure can't leave a half-dormant file (marker appended but not renamed).
|
|
2194
|
+
assertSetupWritePath(`${filePath}.dormant`, perimeterManifest);
|
|
1963
2195
|
const markerResult = writeDormantMarkerToFile(filePath);
|
|
1964
|
-
if (markerResult === 'written')
|
|
1965
|
-
dormantMarkedPaths.push(filePath);
|
|
2196
|
+
if (markerResult === 'written')
|
|
1966
2197
|
log(`Dormant marker written: ${filePath}`);
|
|
2198
|
+
const renameResult = renameSurfaceForDormancy(filePath);
|
|
2199
|
+
if (renameResult === 'renamed' || renameResult === 'replaced') {
|
|
2200
|
+
// dormantMarkedPaths holds post-rename .dormant paths (not the original surface paths).
|
|
2201
|
+
dormantMarkedPaths.push(`${filePath}.dormant`);
|
|
2202
|
+
if (!dormantState.dormantRenamed?.includes(rel)) {
|
|
2203
|
+
dormantState.dormantRenamed = [...(dormantState.dormantRenamed ?? []), rel];
|
|
2204
|
+
dormantStateChanged = true;
|
|
2205
|
+
}
|
|
2206
|
+
log(`Dormant rename: ${filePath} → ${filePath}.dormant`);
|
|
2207
|
+
}
|
|
2208
|
+
else if (renameResult === 'drift') {
|
|
2209
|
+
// Codex P1: an edited .dormant already exists alongside the live file. Preserve
|
|
2210
|
+
// both and flag, rather than overwriting the user's edited dormant copy.
|
|
2211
|
+
syncDriftTensToFire.push(`Edited dormant copy ${rel}.dormant diverges from the live surface for ${dormantAsset.entryId}; left both in place. Resolve manually.`);
|
|
2212
|
+
logErr(`Warning: ${rel}.dormant diverges from the live file; not replacing (possible manual edit).`);
|
|
1967
2213
|
}
|
|
1968
|
-
// 'already-dormant' and 'skipped' are silent no-ops — idempotent.
|
|
1969
2214
|
}
|
|
1970
2215
|
catch (err) {
|
|
1971
|
-
|
|
1972
|
-
logErr(`Warning: could not write dormant marker to ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
2216
|
+
logErr(`Warning: could not dormant-mark ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1973
2217
|
}
|
|
1974
2218
|
}
|
|
1975
2219
|
}
|
|
2220
|
+
if (dormantMarkedPaths.length > 0) {
|
|
2221
|
+
log('Run `pb setup observe --purge` to remove these instead of dormant-renaming.');
|
|
2222
|
+
}
|
|
2223
|
+
if (dormantStateChanged) {
|
|
2224
|
+
try {
|
|
2225
|
+
saveAuthoringSyncState(pbDir, dormantState);
|
|
2226
|
+
}
|
|
2227
|
+
catch (err) {
|
|
2228
|
+
logErr(`Warning: could not persist dormancy state — ${err instanceof Error ? err.message : String(err)}`);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
1976
2231
|
}
|
|
1977
|
-
//
|
|
1978
|
-
//
|
|
1979
|
-
//
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2232
|
+
// 8b. Raise-cleanup (WP-426 E4): for assets active this run, drop any orphan
|
|
2233
|
+
// <surface>.dormant left by a prior lowering (active surface was re-projected
|
|
2234
|
+
// fresh above). User-edited dormant copies are preserved + flagged. Fail-open.
|
|
2235
|
+
if (writeMode) {
|
|
2236
|
+
const raiseState = loadAuthoringSyncState(pbDir);
|
|
2237
|
+
let raiseStateChanged = false;
|
|
2238
|
+
const dormantIds = new Set(dormantDbAssetRows.map((a) => a.entryId));
|
|
2239
|
+
const activeRows = dbAssetRows.filter((a) => !dormantIds.has(a.entryId) &&
|
|
2240
|
+
(a.assetKind === 'skill' || a.assetKind === 'rule' || a.assetKind === 'hook'));
|
|
2241
|
+
for (const asset of activeRows) {
|
|
2242
|
+
// Codex P2: if this asset's body fetch failed, the write loop did NOT reproject its
|
|
2243
|
+
// surfaces this run (same skip the authoring loop applies), so there's no fresh surface
|
|
2244
|
+
// to reconcile against — skip raise-cleanup to avoid acting on stale content.
|
|
2245
|
+
if (bodyFetchFailedEntryIds.has(asset.entryId))
|
|
2246
|
+
continue;
|
|
2247
|
+
for (const { path: filePath, surface } of deriveDormantFilePaths(asset, cwd)) {
|
|
2248
|
+
// Codex P2: honor the run's target set (parity with the dormant pass) — a
|
|
2249
|
+
// `--surfaces cursor` run must not touch .claude/.codex .dormant files, and surfaces
|
|
2250
|
+
// outside the manifest are skipped silently (no false "raise-cleanup failed" warning).
|
|
2251
|
+
if (!allowedTargets.has(surface))
|
|
2252
|
+
continue;
|
|
2253
|
+
const rel = relative(cwd, filePath);
|
|
2254
|
+
try {
|
|
2255
|
+
assertSetupWritePath(filePath, perimeterManifest); // WP-426 E4: perimeter before any FS mutation (parity with the dormant pass)
|
|
2256
|
+
// E4 spec parity (review): restoreSurfaceFromDormant deletes the .dormant
|
|
2257
|
+
// sibling, so guard that delete TARGET too — exactly as the lowering pass
|
|
2258
|
+
// guards both filePath and ${filePath}.dormant.
|
|
2259
|
+
assertSetupWritePath(`${filePath}.dormant`, perimeterManifest);
|
|
2260
|
+
const r = restoreSurfaceFromDormant(filePath);
|
|
2261
|
+
if (r === 'restored') {
|
|
2262
|
+
log(`Raised: removed superseded ${filePath}.dormant`);
|
|
2263
|
+
}
|
|
2264
|
+
else if (r === 'orphan-drift') {
|
|
2265
|
+
syncDriftTensToFire.push(`Dormant copy ${rel}.dormant was edited while dormant for ${asset.entryId}; preserved on raise. Resolve manually.`);
|
|
2266
|
+
logErr(`Warning: ${rel}.dormant differs from the freshly raised projection; left in place.`);
|
|
2267
|
+
}
|
|
2268
|
+
// Prune the registry only when no .dormant sibling remains for this surface:
|
|
2269
|
+
// • 'restored' removed it, or
|
|
2270
|
+
// • it was already gone (manual .dormant→live reactivation; 'skipped', no .dormant).
|
|
2271
|
+
// Carry-over obligation (Phase 3) + Codex P1: the manual-reactivation case must
|
|
2272
|
+
// leave the hands-off set so a future lowering can re-evaluate the surface.
|
|
2273
|
+
// Codex P2: but if a .dormant STILL exists (surface-filtered 'skipped' with no
|
|
2274
|
+
// fresh live file, or a preserved 'orphan-drift'), KEEP the registry evidence —
|
|
2275
|
+
// otherwise a later manual .dormant→live reactivation would not be recognized as
|
|
2276
|
+
// previouslyRenamed and would be silently re-dormanted on the next lowering.
|
|
2277
|
+
if (!existsSync(`${filePath}.dormant`)) {
|
|
2278
|
+
if (raiseState.dormantRenamed?.includes(rel)) {
|
|
2279
|
+
raiseState.dormantRenamed = raiseState.dormantRenamed.filter((p) => p !== rel);
|
|
2280
|
+
raiseStateChanged = true;
|
|
2281
|
+
}
|
|
2282
|
+
if (raiseState.dormantReactivated?.includes(rel)) {
|
|
2283
|
+
raiseState.dormantReactivated = raiseState.dormantReactivated.filter((p) => p !== rel);
|
|
2284
|
+
raiseStateChanged = true;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
catch (err) {
|
|
2289
|
+
logErr(`Warning: raise-cleanup failed for ${filePath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1996
2292
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
const names = forkedPaths.join(', ');
|
|
2001
|
-
kernelCallWithSession('chain.createEntry', {
|
|
2002
|
-
collectionSlug: 'tensions',
|
|
2003
|
-
name: `TEN: handshake drift — ${forkedPaths.length} adapter(s) forked, sync blocked`,
|
|
2004
|
-
// Drift audit TENs intentionally stay draft — they need explicit human review,
|
|
2005
|
-
// not auto-commit, even in Open mode (mirrors smart-capture.ts recordCommitFailure).
|
|
2006
|
-
status: 'draft',
|
|
2007
|
-
data: {
|
|
2008
|
-
description: `pb handshake --apply encountered forked adapters that blocked sync. Files: ${names}. Use --force to overwrite or resolve drift manually.`,
|
|
2009
|
-
},
|
|
2010
|
-
sessionId: session.sessionId,
|
|
2011
|
-
createdBy: `agent:${session.sessionId}`,
|
|
2012
|
-
}).catch(() => { });
|
|
2293
|
+
if (raiseStateChanged) {
|
|
2294
|
+
try {
|
|
2295
|
+
saveAuthoringSyncState(pbDir, raiseState);
|
|
2013
2296
|
}
|
|
2297
|
+
catch { /* fail-open */ }
|
|
2014
2298
|
}
|
|
2015
2299
|
}
|
|
2300
|
+
// 8. User-owned files left untouched are NOT drift (TEN-2150).
|
|
2301
|
+
// A marker-less file at an adapter path is the user's own file: handshake never
|
|
2302
|
+
// wrote it, so there is nothing to "sync" and no draft TEN is logged. These files
|
|
2303
|
+
// are surfaced benignly under "Skipped:" and relayed to the connect screen via
|
|
2304
|
+
// report.userOwnedSkipped. (The legitimate "tampered" and authoring-sync drift
|
|
2305
|
+
// signals below are unaffected.)
|
|
2016
2306
|
if (syncDriftTensToFire.length > 0) {
|
|
2017
2307
|
const session = readSession();
|
|
2018
2308
|
if (session) {
|
|
@@ -2031,10 +2321,10 @@ export async function runHandshake(options = {}) {
|
|
|
2031
2321
|
}
|
|
2032
2322
|
}
|
|
2033
2323
|
}
|
|
2034
|
-
// 8. Case-collision TENs (WP-379 S5b)
|
|
2324
|
+
// 8. Case-collision TENs (WP-379 S5b).
|
|
2035
2325
|
// These are distinct from drift TENs: they record ambiguous filename collisions
|
|
2036
|
-
// where the "newest mtime wins" heuristic was applied. They fire
|
|
2037
|
-
//
|
|
2326
|
+
// where the "newest mtime wins" heuristic was applied. They always fire on a
|
|
2327
|
+
// detected collision (collision is a data quality issue, not a drift issue).
|
|
2038
2328
|
if (collisionTensToFire.length > 0) {
|
|
2039
2329
|
const session = readSession();
|
|
2040
2330
|
if (session) {
|
|
@@ -2110,7 +2400,7 @@ export async function runHandshake(options = {}) {
|
|
|
2110
2400
|
registryStale,
|
|
2111
2401
|
preview: preview ? true : undefined,
|
|
2112
2402
|
previewPlan: preview && previewPlan.length > 0 ? previewPlan : undefined,
|
|
2113
|
-
|
|
2403
|
+
userOwnedSkipped: userOwnedSkipped.length > 0 ? userOwnedSkipped : undefined,
|
|
2114
2404
|
// WP-421 S3: three-bucket drift report (doneWhen #17). PB-managed-clean is
|
|
2115
2405
|
// the count of files whose marker + hash matched. Tampered files were
|
|
2116
2406
|
// resolved (adopted/reverted) above and are reported separately.
|
|
@@ -2122,5 +2412,10 @@ export async function runHandshake(options = {}) {
|
|
|
2122
2412
|
process.stdout.write('\n');
|
|
2123
2413
|
process.stdout.write(formatHandshakeReport(report) + '\n');
|
|
2124
2414
|
}
|
|
2415
|
+
// Return the report so non-UI callers (e.g. prepareConnectContext) can surface
|
|
2416
|
+
// skipped/user-owned files without re-deriving the skip logic (TEN-2107).
|
|
2417
|
+
return report;
|
|
2125
2418
|
}
|
|
2419
|
+
// WP-426 E3/E4: exported test-only surface (not part of the public CLI API).
|
|
2420
|
+
export const __test = { setupAuthoringPath, renameSurfaceForDormancy, restoreSurfaceFromDormant, loadAuthoringSyncState, MARKER };
|
|
2126
2421
|
//# sourceMappingURL=handshake.js.map
|