@bigking67/pi-67 0.10.5 → 0.10.7
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/CHANGELOG.md +24 -0
- package/README.md +12 -2
- package/package.json +1 -1
- package/schemas/pi67-state.schema.json +26 -0
- package/scripts/check.mjs +32 -2
- package/src/commands/doctor.mjs +5 -3
- package/src/commands/install.mjs +20 -1
- package/src/commands/report.mjs +24 -3
- package/src/commands/skills.mjs +11 -6
- package/src/commands/status.mjs +5 -1
- package/src/commands/update.mjs +30 -3
- package/src/lib/distro-scripts.mjs +4 -0
- package/src/lib/settings-runtime-clean.mjs +20 -0
- package/src/lib/settings-runtime-state.mjs +167 -0
- package/src/lib/skill-policy.mjs +5 -3
- package/src/lib/state-store.mjs +14 -2
- package/src/lib/update-plan.mjs +27 -6
- package/src/tools/settings-runtime-state-filter.mjs +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.7]
|
|
4
|
+
|
|
5
|
+
- Migrates `settings.json.lastChangelogVersion` into ignored manager state at
|
|
6
|
+
`~/.pi/pi67/state.json`, normalizes the runtime-only marker out of
|
|
7
|
+
`settings.json`, and installs a local Git clean filter so future marker
|
|
8
|
+
writes cannot be carried into normal diffs or commits.
|
|
9
|
+
- Renames shared-skill drift in human-facing output from "conflicts" to
|
|
10
|
+
"preserved user-modified" while keeping the JSON `conflicts` compatibility
|
|
11
|
+
field.
|
|
12
|
+
|
|
13
|
+
## [0.10.6]
|
|
14
|
+
|
|
15
|
+
- Adds runtime request retry to the canonical `xtalpi-pi-tools` provider path,
|
|
16
|
+
including attempt/retry/suppression telemetry in debug JSONL output.
|
|
17
|
+
- Adds final-answer compliance repair to Bash and PowerShell xtalpi smoke:
|
|
18
|
+
validated tool runs that only miss required final text get one `--no-tools`
|
|
19
|
+
repair pass instead of rerunning tools.
|
|
20
|
+
- Accepts `pi-67 doctor --no-skill-list` and raises doctor skill-list timeout
|
|
21
|
+
defaults to 60 seconds.
|
|
22
|
+
- Supports command-level `pi-67 report --json`, emitting either a dry-run JSON
|
|
23
|
+
object or the generated report JSON.
|
|
24
|
+
- Runs full `scripts/pi67-smoke.sh --ci` in the npm publish workflow before
|
|
25
|
+
pack/publish.
|
|
26
|
+
|
|
3
27
|
## [0.10.5]
|
|
4
28
|
|
|
5
29
|
- Uses `git fetch` plus `git merge --ff-only FETCH_HEAD` in Bash and
|
package/README.md
CHANGED
|
@@ -52,6 +52,14 @@ For automation, `pi-67 update --check --json` includes explicit `actions`,
|
|
|
52
52
|
`blocked`, and `warnings` arrays. Each action lists planned writes and preserved
|
|
53
53
|
paths, so update previews stay auditable instead of relying on prose output.
|
|
54
54
|
|
|
55
|
+
Long or slow doctor runs can skip Pi's live skill listing while still checking
|
|
56
|
+
local metadata, config, provider/model, shared skill files, and endpoint
|
|
57
|
+
contracts:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pi-67 doctor --no-skill-list
|
|
61
|
+
```
|
|
62
|
+
|
|
55
63
|
If the local manager may be stale, run the latest npm package for one repair:
|
|
56
64
|
|
|
57
65
|
```bash
|
|
@@ -121,8 +129,9 @@ The explicit theme setter also writes a runtime backup before changing
|
|
|
121
129
|
`settings.json`; normal update never changes the selected theme.
|
|
122
130
|
|
|
123
131
|
The manager writes lightweight state outside the repo at `~/.pi/pi67/state.json`.
|
|
124
|
-
It records versions, paths, theme, provider/model,
|
|
125
|
-
|
|
132
|
+
It records versions, paths, theme, provider/model, commit information, and
|
|
133
|
+
runtime-only UI markers such as `settings.json.lastChangelogVersion` after
|
|
134
|
+
migrating them out of tracked config; it does not store API keys.
|
|
126
135
|
|
|
127
136
|
## Main commands
|
|
128
137
|
|
|
@@ -143,6 +152,7 @@ pi-67 doctor
|
|
|
143
152
|
pi-67 smoke --quick
|
|
144
153
|
pi-67 status
|
|
145
154
|
pi-67 report
|
|
155
|
+
pi-67 report --json
|
|
146
156
|
pi-67 version
|
|
147
157
|
pi-67 xtalpi health
|
|
148
158
|
pi-67 xtalpi smoke --quick
|
package/package.json
CHANGED
|
@@ -25,6 +25,32 @@
|
|
|
25
25
|
},
|
|
26
26
|
"lastTheme": {
|
|
27
27
|
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
"runtimeMarkers": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"description": "Runtime-only UI markers migrated out of tracked runtime config files.",
|
|
32
|
+
"properties": {
|
|
33
|
+
"lastChangelogVersion": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["value", "storage"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"value": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"source": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"storage": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"updatedAt": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"additionalProperties": true
|
|
28
54
|
}
|
|
29
55
|
},
|
|
30
56
|
"additionalProperties": true
|
package/scripts/check.mjs
CHANGED
|
@@ -8,6 +8,11 @@ import { commandCandidatesForPlatform } from "../src/lib/shell-runner.mjs";
|
|
|
8
8
|
import { parseCommandOptions, splitGlobalArgs } from "../src/lib/args.mjs";
|
|
9
9
|
import { readExtensionRegistry, validateExtensionRegistry } from "../src/lib/extension-registry.mjs";
|
|
10
10
|
import { buildPlanDecisions, classifyGitShort } from "../src/lib/update-plan.mjs";
|
|
11
|
+
import {
|
|
12
|
+
mergeSettingsRuntimeMarkerIntoState,
|
|
13
|
+
settingsRuntimeMarkerFromObject,
|
|
14
|
+
stripSettingsRuntimeMarkerText,
|
|
15
|
+
} from "../src/lib/settings-runtime-state.mjs";
|
|
11
16
|
import {
|
|
12
17
|
beginUpdateLifecycle,
|
|
13
18
|
inspectLegacyConflictBackup,
|
|
@@ -35,6 +40,7 @@ runCliHelpContractSelfTests();
|
|
|
35
40
|
runPublishTargetSelfTests();
|
|
36
41
|
runShellRunnerSelfTests();
|
|
37
42
|
runExtensionRegistrySelfTests();
|
|
43
|
+
runSettingsRuntimeStateSelfTests();
|
|
38
44
|
runUpdatePlanSelfTests();
|
|
39
45
|
runUpdateSafetySelfTests();
|
|
40
46
|
|
|
@@ -59,6 +65,14 @@ function runArgsSelfTests() {
|
|
|
59
65
|
parseCommandOptions(["--help"], { bools: [] }).options.help,
|
|
60
66
|
"command option parser must accept --help for every command",
|
|
61
67
|
);
|
|
68
|
+
assert(
|
|
69
|
+
parseCommandOptions(["--no-skill-list"], { bools: ["no-skill-list"] }).options.noSkillList,
|
|
70
|
+
"command option parser must accept doctor --no-skill-list",
|
|
71
|
+
);
|
|
72
|
+
assert(
|
|
73
|
+
parseCommandOptions(["--json"], { bools: ["json"] }).options.json,
|
|
74
|
+
"command option parser must accept command-level --json",
|
|
75
|
+
);
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
function runCliHelpContractSelfTests() {
|
|
@@ -314,6 +328,22 @@ function runExtensionRegistrySelfTests() {
|
|
|
314
328
|
);
|
|
315
329
|
}
|
|
316
330
|
|
|
331
|
+
function runSettingsRuntimeStateSelfTests() {
|
|
332
|
+
const input = "{\n \"lastChangelogVersion\": \"0.80.3\",\n \"theme\": \"gruvbox-dark\"\n}\n";
|
|
333
|
+
const stripped = JSON.parse(stripSettingsRuntimeMarkerText(input));
|
|
334
|
+
assert(
|
|
335
|
+
stripped.lastChangelogVersion === undefined && stripped.theme === "gruvbox-dark",
|
|
336
|
+
"settings runtime clean filter must remove only lastChangelogVersion",
|
|
337
|
+
);
|
|
338
|
+
const marker = settingsRuntimeMarkerFromObject({ lastChangelogVersion: "0.80.3" });
|
|
339
|
+
const state = mergeSettingsRuntimeMarkerIntoState({ schema: "pi67.state.v1" }, marker, "2026-07-08T00:00:00.000Z");
|
|
340
|
+
assert(
|
|
341
|
+
state.runtimeMarkers?.lastChangelogVersion?.value === "0.80.3" &&
|
|
342
|
+
state.runtimeMarkers.lastChangelogVersion.storage === "state.json",
|
|
343
|
+
"settings runtime marker must migrate into state runtimeMarkers",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
317
347
|
function runUpdatePlanSelfTests() {
|
|
318
348
|
assert(
|
|
319
349
|
classifyGitShort(" M settings.json\n?? tmp.txt").preservedRuntime.includes("settings.json"),
|
|
@@ -388,8 +418,8 @@ function runUpdatePlanSelfTests() {
|
|
|
388
418
|
skills: { summary: { missing: 0, conflicts: 2 } },
|
|
389
419
|
});
|
|
390
420
|
assert(
|
|
391
|
-
buildPlanDecisions(sharedSkillConflict).warnings.some((item) => item.includes("
|
|
392
|
-
"shared
|
|
421
|
+
buildPlanDecisions(sharedSkillConflict).warnings.some((item) => item.includes("preserved user-modified global skills")),
|
|
422
|
+
"preserved user-modified shared skills must warn and preserve by default",
|
|
393
423
|
);
|
|
394
424
|
const strictSharedSkillConflict = decisionsFixture({
|
|
395
425
|
strictSharedSkills: true,
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { isWindows } from "../lib/platform.mjs";
|
|
|
4
4
|
|
|
5
5
|
export async function doctorCommand(ctx, argv) {
|
|
6
6
|
const { options } = parseCommandOptions(argv, {
|
|
7
|
-
bools: ["json", "quiet", "dry-run", "deep-mcp", "strict-shared-skills"],
|
|
7
|
+
bools: ["json", "quiet", "dry-run", "deep-mcp", "no-skill-list", "strict-shared-skills"],
|
|
8
8
|
strings: ["mcp-timeout-ms", "skill-list-timeout-seconds"],
|
|
9
9
|
});
|
|
10
10
|
if (options.help) {
|
|
@@ -19,6 +19,7 @@ export async function doctorCommand(ctx, argv) {
|
|
|
19
19
|
if (options.strictSharedSkills) args.push(isWindows() ? "-StrictSharedSkills" : "--strict-shared-skills");
|
|
20
20
|
if (!isWindows() && options.deepMcp) args.push("--deep-mcp");
|
|
21
21
|
if (!isWindows() && options.mcpTimeoutMs) args.push("--mcp-timeout-ms", options.mcpTimeoutMs);
|
|
22
|
+
if (!isWindows() && options.noSkillList) args.push("--no-skill-list");
|
|
22
23
|
if (options.skillListTimeoutSeconds) {
|
|
23
24
|
args.push(isWindows() ? "-SkillListTimeoutSeconds" : "--skill-list-timeout-seconds", options.skillListTimeoutSeconds);
|
|
24
25
|
}
|
|
@@ -38,9 +39,10 @@ Options:
|
|
|
38
39
|
--quiet Reduce human output where supported.
|
|
39
40
|
--deep-mcp Run deeper MCP probes on POSIX platforms.
|
|
40
41
|
--mcp-timeout-ms N Timeout for deep MCP probes on POSIX platforms.
|
|
42
|
+
--no-skill-list Skip pi skill list on POSIX platforms; accepted as a no-op on Windows.
|
|
41
43
|
--skill-list-timeout-seconds N
|
|
42
|
-
Timeout for pi skill list
|
|
43
|
-
--strict-shared-skills Treat
|
|
44
|
+
Timeout for pi skill list where enabled.
|
|
45
|
+
--strict-shared-skills Treat preserved user-modified shared skills as blocking.
|
|
44
46
|
--dry-run Print the script invocation without running it.
|
|
45
47
|
|
|
46
48
|
Examples:
|
package/src/commands/install.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { runDistroScript } from "../lib/distro-scripts.mjs";
|
|
|
7
7
|
import { isWindows } from "../lib/platform.mjs";
|
|
8
8
|
import { CliError, info, warn } from "../lib/output.mjs";
|
|
9
9
|
import { writeState } from "../lib/state-store.mjs";
|
|
10
|
+
import { migrateSettingsRuntimeState } from "../lib/settings-runtime-state.mjs";
|
|
10
11
|
|
|
11
12
|
export async function installCommand(ctx, argv) {
|
|
12
13
|
const { options } = parseCommandOptions(argv, {
|
|
@@ -47,7 +48,25 @@ export async function installCommand(ctx, argv) {
|
|
|
47
48
|
if (dryRun) args.push("--dry-run");
|
|
48
49
|
runCommand("bash", [path.join(ctx.repoRoot, "install.sh"), ...args], { cwd: ctx.repoRoot, dryRun: false });
|
|
49
50
|
}
|
|
50
|
-
if (!dryRun)
|
|
51
|
+
if (!dryRun) {
|
|
52
|
+
writeState(ctx, "install");
|
|
53
|
+
const runtimeState = migrateSettingsRuntimeState(ctx, {
|
|
54
|
+
normalizeSettingsJson: true,
|
|
55
|
+
installGitFilter: true,
|
|
56
|
+
});
|
|
57
|
+
if (runtimeState.markerFound && runtimeState.stateWritten) {
|
|
58
|
+
info("Migrated settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json");
|
|
59
|
+
}
|
|
60
|
+
if (runtimeState.settingsNormalized) {
|
|
61
|
+
info("Normalized settings.json by removing runtime-only lastChangelogVersion.");
|
|
62
|
+
}
|
|
63
|
+
if (runtimeState.gitFilterInstalled) {
|
|
64
|
+
info("Installed local git clean filter for future settings.json runtime markers.");
|
|
65
|
+
}
|
|
66
|
+
for (const error of runtimeState.errors) {
|
|
67
|
+
warn(`settings runtime marker migration skipped: ${error}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
51
70
|
info("Install finished. Run `pi-67 doctor` next.");
|
|
52
71
|
}
|
|
53
72
|
|
package/src/commands/report.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { parseCommandOptions } from "../lib/args.mjs";
|
|
2
4
|
import { runDistroScript } from "../lib/distro-scripts.mjs";
|
|
3
5
|
import { isWindows } from "../lib/platform.mjs";
|
|
6
|
+
import { printJson } from "../lib/output.mjs";
|
|
4
7
|
|
|
5
8
|
export async function reportCommand(ctx, argv) {
|
|
6
9
|
const { options } = parseCommandOptions(argv, {
|
|
7
|
-
bools: ["dry-run", "no-doctor"],
|
|
10
|
+
bools: ["json", "dry-run", "no-doctor"],
|
|
8
11
|
strings: ["operation", "output"],
|
|
9
12
|
});
|
|
10
13
|
if (options.help) {
|
|
@@ -15,21 +18,39 @@ export async function reportCommand(ctx, argv) {
|
|
|
15
18
|
? ["-AgentDir", ctx.agentDir, "-RepoRoot", ctx.repoRoot, "-SkillsDir", ctx.skillsDir]
|
|
16
19
|
: ["--agent-dir", ctx.agentDir, "--repo-root", ctx.repoRoot, "--skills-dir", ctx.skillsDir];
|
|
17
20
|
if (options.operation) args.push(isWindows() ? "-Operation" : "--operation", options.operation);
|
|
18
|
-
|
|
21
|
+
const outputPath = options.output || path.join(ctx.agentDir, "pi67-report.json");
|
|
22
|
+
const resolvedOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(ctx.repoRoot, outputPath);
|
|
23
|
+
args.push(isWindows() ? "-Output" : "--output", outputPath);
|
|
19
24
|
if (options.noDoctor) args.push(isWindows() ? "-NoDoctor" : "--no-doctor");
|
|
20
25
|
if (ctx.dryRun || options.dryRun) args.push(isWindows() ? "-DryRun" : "--dry-run");
|
|
26
|
+
const json = ctx.json || options.json;
|
|
27
|
+
if (json && (ctx.dryRun || options.dryRun)) {
|
|
28
|
+
printJson({
|
|
29
|
+
schema: "pi67.report-command.v1",
|
|
30
|
+
dryRun: true,
|
|
31
|
+
output: resolvedOutputPath,
|
|
32
|
+
operation: options.operation || "manual",
|
|
33
|
+
doctor: options.noDoctor ? "skipped" : "enabled",
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
21
37
|
runDistroScript(ctx, { sh: "pi67-report.sh", ps1: "pi67-report.ps1" }, args, {
|
|
22
38
|
dryRun: false,
|
|
39
|
+
stdio: json ? "pipe" : "inherit",
|
|
23
40
|
});
|
|
41
|
+
if (json) {
|
|
42
|
+
process.stdout.write(fs.readFileSync(resolvedOutputPath, "utf8"));
|
|
43
|
+
}
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
function printReportHelp() {
|
|
27
47
|
process.stdout.write(`pi-67 report - generate pi67-report.json
|
|
28
48
|
|
|
29
49
|
Usage:
|
|
30
|
-
pi-67 report [--operation NAME] [--output FILE] [--no-doctor] [--dry-run]
|
|
50
|
+
pi-67 report [--json] [--operation NAME] [--output FILE] [--no-doctor] [--dry-run]
|
|
31
51
|
|
|
32
52
|
Options:
|
|
53
|
+
--json Emit the generated report JSON to stdout.
|
|
33
54
|
--operation NAME Operation label to embed in the report.
|
|
34
55
|
--output FILE Output path. Defaults to the distro report location.
|
|
35
56
|
--no-doctor Skip doctor data collection where supported.
|
package/src/commands/skills.mjs
CHANGED
|
@@ -26,9 +26,9 @@ function inventory(ctx, argv) {
|
|
|
26
26
|
keyValue("Target", data.skillsDir);
|
|
27
27
|
keyValue("Identical", data.summary.identical);
|
|
28
28
|
keyValue("Missing", data.summary.missing);
|
|
29
|
-
keyValue("
|
|
29
|
+
keyValue("Preserved user-modified", preservedUserModified(data.summary));
|
|
30
30
|
for (const entry of data.entries.filter((item) => !item.identical)) {
|
|
31
|
-
const status = entry.conflict ? "
|
|
31
|
+
const status = entry.conflict ? "preserved user-modified" : "missing";
|
|
32
32
|
info(`${status}: ${entry.name}`);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -43,7 +43,7 @@ function plan(ctx, argv) {
|
|
|
43
43
|
keyValue("Target", data.skillsDir);
|
|
44
44
|
keyValue("Selected", data.selected.length === 0 ? "all changed skills" : data.selected.join(", "));
|
|
45
45
|
keyValue("Missing", data.actions.filter((item) => item.action === "copy-missing").length);
|
|
46
|
-
keyValue("
|
|
46
|
+
keyValue("Preserved user-modified", data.actions.filter((item) => item.action === "preserve-conflict").length);
|
|
47
47
|
for (const action of data.actions) {
|
|
48
48
|
const command = action.conflict
|
|
49
49
|
? `inspect: pi-67 skills diff ${action.name}; explicit sync: pi-67 skills sync ${action.name} --dry-run`
|
|
@@ -100,7 +100,11 @@ function migrate(ctx, argv) {
|
|
|
100
100
|
section("pi-67 skills migrate preview");
|
|
101
101
|
warn("migrate is intentionally dry-run in the npm manager; use `pi-67 skills sync` to copy missing skills.");
|
|
102
102
|
keyValue("Missing", data.summary.missing);
|
|
103
|
-
keyValue("
|
|
103
|
+
keyValue("Preserved user-modified", preservedUserModified(data.summary));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function preservedUserModified(summary) {
|
|
107
|
+
return summary?.preservedUserModified ?? summary?.conflicts ?? 0;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
function printSkillsHelp() {
|
|
@@ -115,8 +119,9 @@ Usage:
|
|
|
115
119
|
|
|
116
120
|
Safety:
|
|
117
121
|
Missing skills are copied by default. Existing different global skills are
|
|
118
|
-
preserved unless you name the skill explicitly and pass
|
|
119
|
-
overwrite is intentionally
|
|
122
|
+
preserved as user-modified unless you name the skill explicitly and pass
|
|
123
|
+
--yes. Bulk overwrite of preserved user-modified skills is intentionally
|
|
124
|
+
blocked.
|
|
120
125
|
|
|
121
126
|
Examples:
|
|
122
127
|
pi-67 skills inventory
|
package/src/commands/status.mjs
CHANGED
|
@@ -21,7 +21,11 @@ export async function statusCommand(ctx, argv) {
|
|
|
21
21
|
keyValue("Provider", plan.settings.defaultProvider || "unset");
|
|
22
22
|
keyValue("Model", plan.settings.defaultModel || "unset");
|
|
23
23
|
keyValue("Theme", plan.settings.theme || "unset");
|
|
24
|
-
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${plan.skills
|
|
24
|
+
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${preservedUserModified(plan.skills)} preserved user-modified`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function preservedUserModified(skills) {
|
|
28
|
+
return skills?.preservedUserModified ?? skills?.conflicts ?? 0;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
function printStatusHelp() {
|
package/src/commands/update.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { isWindows } from "../lib/platform.mjs";
|
|
|
6
6
|
import { runCommand } from "../lib/shell-runner.mjs";
|
|
7
7
|
import { writeState } from "../lib/state-store.mjs";
|
|
8
8
|
import { beginUpdateLifecycle } from "../lib/update-safety.mjs";
|
|
9
|
+
import { migrateSettingsRuntimeState } from "../lib/settings-runtime-state.mjs";
|
|
9
10
|
|
|
10
11
|
export async function updateCommand(ctx, argv) {
|
|
11
12
|
const { options } = parseCommandOptions(argv, {
|
|
@@ -65,7 +66,10 @@ export async function updateCommand(ctx, argv) {
|
|
|
65
66
|
: buildBashUpdateArgs(ctx, options, dryRun);
|
|
66
67
|
try {
|
|
67
68
|
runDistroScript(ctx, { sh: "pi67-update.sh", ps1: "pi67-update.ps1" }, args, { dryRun: false });
|
|
68
|
-
if (!dryRun)
|
|
69
|
+
if (!dryRun) {
|
|
70
|
+
writeState(ctx, options.repair ? "repair" : "update");
|
|
71
|
+
reportSettingsRuntimeStateMigration(ctx);
|
|
72
|
+
}
|
|
69
73
|
} finally {
|
|
70
74
|
lifecycle.release();
|
|
71
75
|
}
|
|
@@ -82,6 +86,25 @@ export async function updateCommand(ctx, argv) {
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
function reportSettingsRuntimeStateMigration(ctx) {
|
|
90
|
+
const result = migrateSettingsRuntimeState(ctx, {
|
|
91
|
+
normalizeSettingsJson: true,
|
|
92
|
+
installGitFilter: true,
|
|
93
|
+
});
|
|
94
|
+
if (result.markerFound && result.stateWritten) {
|
|
95
|
+
info("Migrated settings.json lastChangelogVersion to ignored state: ~/.pi/pi67/state.json");
|
|
96
|
+
}
|
|
97
|
+
if (result.settingsNormalized) {
|
|
98
|
+
info("Normalized settings.json by removing runtime-only lastChangelogVersion.");
|
|
99
|
+
}
|
|
100
|
+
if (result.gitFilterInstalled) {
|
|
101
|
+
info("Installed local git clean filter for future settings.json runtime markers.");
|
|
102
|
+
}
|
|
103
|
+
for (const error of result.errors) {
|
|
104
|
+
warn(`settings runtime marker migration skipped: ${error}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
function buildBashUpdateArgs(ctx, options, dryRun) {
|
|
86
109
|
const args = ["--agent-dir", ctx.agentDir, "--repo-root", ctx.repoRoot, "--skills-dir", ctx.skillsDir];
|
|
87
110
|
if (dryRun) args.push("--dry-run");
|
|
@@ -128,7 +151,7 @@ function printPlan(plan) {
|
|
|
128
151
|
keyValue("Model", plan.settings.defaultModel || "unset");
|
|
129
152
|
keyValue("Theme", plan.settings.theme || "unset");
|
|
130
153
|
keyValue("Theme installed", plan.settings.themeInstalled ? "yes" : "no");
|
|
131
|
-
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${plan.skills
|
|
154
|
+
keyValue("Shared skills", `${plan.skills.identical} ok, ${plan.skills.missing} missing, ${preservedUserModified(plan.skills)} preserved user-modified`);
|
|
132
155
|
for (const repo of plan.external) {
|
|
133
156
|
keyValue(`External ${repo.name}`, repo.exists ? `${repo.git?.dirty ? "dirty" : "clean"} ${repo.git?.commit || ""}` : "missing");
|
|
134
157
|
}
|
|
@@ -153,6 +176,10 @@ function printPlan(plan) {
|
|
|
153
176
|
if (!plan.git?.dirty) pass("update check completed without local dirty blocker");
|
|
154
177
|
}
|
|
155
178
|
|
|
179
|
+
function preservedUserModified(skills) {
|
|
180
|
+
return skills?.preservedUserModified ?? skills?.conflicts ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
156
183
|
function printUpdateHelp() {
|
|
157
184
|
process.stdout.write(`pi-67 update - update pi-67 safely
|
|
158
185
|
|
|
@@ -167,7 +194,7 @@ Options:
|
|
|
167
194
|
--no-remote Skip remote git/npm registry checks where supported.
|
|
168
195
|
--no-npm Skip npm package sync in the distro updater.
|
|
169
196
|
--allow-dirty Let the script-level updater handle non-runtime dirty files.
|
|
170
|
-
--strict-shared-skills Treat
|
|
197
|
+
--strict-shared-skills Treat preserved user-modified shared skills as blocking.
|
|
171
198
|
--include-pi Also run upstream \`pi update --all\` when paired with --yes.
|
|
172
199
|
--include-external Report explicit external repo update commands.
|
|
173
200
|
--all Alias for --include-pi --include-external.
|
|
@@ -17,11 +17,15 @@ export function runDistroScript(ctx, names, args = [], options = {}) {
|
|
|
17
17
|
cwd: ctx.repoRoot,
|
|
18
18
|
dryRun: options.dryRun,
|
|
19
19
|
env: options.env,
|
|
20
|
+
stdio: options.stdio,
|
|
21
|
+
timeoutMs: options.timeoutMs,
|
|
20
22
|
});
|
|
21
23
|
}
|
|
22
24
|
return runCommand("bash", [file, ...args], {
|
|
23
25
|
cwd: ctx.repoRoot,
|
|
24
26
|
dryRun: options.dryRun,
|
|
25
27
|
env: options.env,
|
|
28
|
+
stdio: options.stdio,
|
|
29
|
+
timeoutMs: options.timeoutMs,
|
|
26
30
|
});
|
|
27
31
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const SETTINGS_RUNTIME_MARKER_KEY = "lastChangelogVersion";
|
|
2
|
+
|
|
3
|
+
export function stripSettingsRuntimeMarker(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
5
|
+
const next = { ...value };
|
|
6
|
+
delete next[SETTINGS_RUNTIME_MARKER_KEY];
|
|
7
|
+
return next;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function stripSettingsRuntimeMarkerText(text) {
|
|
11
|
+
try {
|
|
12
|
+
return stableSettingsJson(stripSettingsRuntimeMarker(JSON.parse(String(text || "").replace(/^\uFEFF/, ""))));
|
|
13
|
+
} catch {
|
|
14
|
+
return String(text || "");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stableSettingsJson(value) {
|
|
19
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { captureCommand } from "./shell-runner.mjs";
|
|
4
|
+
import { isGitRepo } from "./git.mjs";
|
|
5
|
+
import { readJsonFileIfExists, writeJsonAtomic } from "./config-json.mjs";
|
|
6
|
+
import {
|
|
7
|
+
SETTINGS_RUNTIME_MARKER_KEY,
|
|
8
|
+
stableSettingsJson,
|
|
9
|
+
stripSettingsRuntimeMarker,
|
|
10
|
+
stripSettingsRuntimeMarkerText,
|
|
11
|
+
} from "./settings-runtime-clean.mjs";
|
|
12
|
+
|
|
13
|
+
export const SETTINGS_RUNTIME_FILTER_NAME = "pi67-settings-runtime-state";
|
|
14
|
+
export const SETTINGS_RUNTIME_FILTER_SCRIPT = "packages/pi67-cli/src/tools/settings-runtime-state-filter.mjs";
|
|
15
|
+
|
|
16
|
+
export { SETTINGS_RUNTIME_MARKER_KEY, stripSettingsRuntimeMarker, stripSettingsRuntimeMarkerText };
|
|
17
|
+
|
|
18
|
+
export function settingsRuntimeMarkerFromObject(settings) {
|
|
19
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return null;
|
|
20
|
+
const value = settings[SETTINGS_RUNTIME_MARKER_KEY];
|
|
21
|
+
if (value === undefined || value === null || String(value).trim() === "") return null;
|
|
22
|
+
return {
|
|
23
|
+
key: SETTINGS_RUNTIME_MARKER_KEY,
|
|
24
|
+
value: String(value),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function settingsRuntimeMarkerFromState(state) {
|
|
29
|
+
const marker = state?.runtimeMarkers?.[SETTINGS_RUNTIME_MARKER_KEY];
|
|
30
|
+
if (!marker || marker.value === undefined || marker.value === null || String(marker.value).trim() === "") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
key: SETTINGS_RUNTIME_MARKER_KEY,
|
|
35
|
+
value: String(marker.value),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function mergeSettingsRuntimeMarkerIntoState(state, marker, now = new Date().toISOString()) {
|
|
40
|
+
const next = { ...(state || {}) };
|
|
41
|
+
const runtimeMarkers = { ...(next.runtimeMarkers || {}) };
|
|
42
|
+
if (marker?.value) {
|
|
43
|
+
runtimeMarkers[SETTINGS_RUNTIME_MARKER_KEY] = {
|
|
44
|
+
value: marker.value,
|
|
45
|
+
source: "settings.json",
|
|
46
|
+
storage: "state.json",
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(runtimeMarkers).length > 0) {
|
|
51
|
+
next.runtimeMarkers = runtimeMarkers;
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function migrateSettingsRuntimeState(ctx, options = {}) {
|
|
57
|
+
const normalizeSettingsJson = Boolean(options.normalizeSettingsJson);
|
|
58
|
+
const installGitFilter = Boolean(options.installGitFilter);
|
|
59
|
+
const dryRun = Boolean(options.dryRun);
|
|
60
|
+
const settingsPath = path.join(ctx.agentDir, "settings.json");
|
|
61
|
+
const statePath = path.join(ctx.stateDir, "state.json");
|
|
62
|
+
const result = {
|
|
63
|
+
schema: "pi67.settings-runtime-state.v1",
|
|
64
|
+
settingsPath,
|
|
65
|
+
statePath,
|
|
66
|
+
markerFound: false,
|
|
67
|
+
markerValue: "",
|
|
68
|
+
stateWritten: false,
|
|
69
|
+
settingsNormalized: false,
|
|
70
|
+
gitFilterInstalled: false,
|
|
71
|
+
skipped: [],
|
|
72
|
+
errors: [],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(settingsPath)) {
|
|
76
|
+
result.skipped.push("settings.json missing");
|
|
77
|
+
} else {
|
|
78
|
+
let settingsText = "";
|
|
79
|
+
let settings = null;
|
|
80
|
+
try {
|
|
81
|
+
settingsText = fs.readFileSync(settingsPath, "utf8").replace(/^\uFEFF/, "");
|
|
82
|
+
settings = JSON.parse(settingsText);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
result.errors.push(`settings.json is not valid JSON: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const marker = settingsRuntimeMarkerFromObject(settings);
|
|
88
|
+
if (marker) {
|
|
89
|
+
result.markerFound = true;
|
|
90
|
+
result.markerValue = marker.value;
|
|
91
|
+
if (!dryRun) {
|
|
92
|
+
const previous = readJsonFileIfExists(statePath) || {};
|
|
93
|
+
writeJsonAtomic(statePath, mergeSettingsRuntimeMarkerIntoState(previous, marker));
|
|
94
|
+
result.stateWritten = true;
|
|
95
|
+
}
|
|
96
|
+
if (normalizeSettingsJson) {
|
|
97
|
+
const normalizedText = stableSettingsJson(stripSettingsRuntimeMarker(settings));
|
|
98
|
+
if (normalizedText !== settingsText) {
|
|
99
|
+
if (!dryRun) {
|
|
100
|
+
fs.writeFileSync(settingsPath, normalizedText, "utf8");
|
|
101
|
+
}
|
|
102
|
+
result.settingsNormalized = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
result.skipped.push("settings.json runtime marker absent");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (installGitFilter) {
|
|
111
|
+
const filterResult = installSettingsRuntimeGitFilter(ctx, { dryRun });
|
|
112
|
+
result.gitFilterInstalled = filterResult.installed;
|
|
113
|
+
if (filterResult.skipped) result.skipped.push(filterResult.skipped);
|
|
114
|
+
if (filterResult.error) result.errors.push(filterResult.error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function installSettingsRuntimeGitFilter(ctx, options = {}) {
|
|
121
|
+
const result = {
|
|
122
|
+
installed: false,
|
|
123
|
+
skipped: "",
|
|
124
|
+
error: "",
|
|
125
|
+
};
|
|
126
|
+
if (options.dryRun) {
|
|
127
|
+
result.installed = true;
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
if (!isGitRepo(ctx.repoRoot)) {
|
|
131
|
+
result.skipped = "repo root is not a git checkout";
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
const scriptPath = path.join(ctx.repoRoot, SETTINGS_RUNTIME_FILTER_SCRIPT);
|
|
135
|
+
if (!fs.existsSync(scriptPath)) {
|
|
136
|
+
result.skipped = `settings runtime filter script missing: ${SETTINGS_RUNTIME_FILTER_SCRIPT}`;
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const cleanCommand = `node ${SETTINGS_RUNTIME_FILTER_SCRIPT} --clean`;
|
|
141
|
+
const clean = captureCommand("git", [
|
|
142
|
+
"-C",
|
|
143
|
+
ctx.repoRoot,
|
|
144
|
+
"config",
|
|
145
|
+
"--local",
|
|
146
|
+
`filter.${SETTINGS_RUNTIME_FILTER_NAME}.clean`,
|
|
147
|
+
cleanCommand,
|
|
148
|
+
]);
|
|
149
|
+
if (!clean.ok) {
|
|
150
|
+
result.error = clean.stderr || clean.error || "failed to configure settings runtime clean filter";
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
const required = captureCommand("git", [
|
|
154
|
+
"-C",
|
|
155
|
+
ctx.repoRoot,
|
|
156
|
+
"config",
|
|
157
|
+
"--local",
|
|
158
|
+
`filter.${SETTINGS_RUNTIME_FILTER_NAME}.required`,
|
|
159
|
+
"false",
|
|
160
|
+
]);
|
|
161
|
+
if (!required.ok) {
|
|
162
|
+
result.error = required.stderr || required.error || "failed to configure settings runtime filter required=false";
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
result.installed = true;
|
|
166
|
+
return result;
|
|
167
|
+
}
|
package/src/lib/skill-policy.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export function inventorySkills(ctx) {
|
|
|
16
16
|
const conflict = targetExists && !identical;
|
|
17
17
|
return { name, source, target, sourceHash, targetExists, targetHash, identical, conflict };
|
|
18
18
|
});
|
|
19
|
+
const conflicts = entries.filter((entry) => entry.conflict).length;
|
|
19
20
|
return {
|
|
20
21
|
schema: "pi67.skills-inventory.v1",
|
|
21
22
|
sourceRoot,
|
|
@@ -24,7 +25,8 @@ export function inventorySkills(ctx) {
|
|
|
24
25
|
source: entries.length,
|
|
25
26
|
missing: entries.filter((entry) => !entry.targetExists).length,
|
|
26
27
|
identical: entries.filter((entry) => entry.identical).length,
|
|
27
|
-
conflicts
|
|
28
|
+
conflicts,
|
|
29
|
+
preservedUserModified: conflicts,
|
|
28
30
|
},
|
|
29
31
|
entries,
|
|
30
32
|
};
|
|
@@ -79,7 +81,7 @@ export function planSkills(ctx, { names = [] } = {}) {
|
|
|
79
81
|
conflict: entry.conflict,
|
|
80
82
|
action: entry.conflict ? "preserve-conflict" : "copy-missing",
|
|
81
83
|
reason: entry.conflict
|
|
82
|
-
? "target differs; default update preserves
|
|
84
|
+
? "target differs; default update preserves this user-modified global skill"
|
|
83
85
|
: "target is missing and can be copied safely",
|
|
84
86
|
}));
|
|
85
87
|
return {
|
|
@@ -112,7 +114,7 @@ export function syncSkills(ctx, { dryRun = false, names = [], yes = false } = {}
|
|
|
112
114
|
action: "warn",
|
|
113
115
|
reason: targeted
|
|
114
116
|
? "target differs; rerun with --yes to replace this explicitly named skill after backup"
|
|
115
|
-
: "target differs; bulk
|
|
117
|
+
: "target differs; bulk overwrite of preserved user-modified skills is intentionally blocked",
|
|
116
118
|
});
|
|
117
119
|
continue;
|
|
118
120
|
}
|
package/src/lib/state-store.mjs
CHANGED
|
@@ -4,11 +4,18 @@ import { gitStatus } from "./git.mjs";
|
|
|
4
4
|
import { currentTheme } from "./theme-policy.mjs";
|
|
5
5
|
import { readJsonFileIfExists, writeJsonAtomic } from "./config-json.mjs";
|
|
6
6
|
import { readCliPackageJson, readTextIfExists } from "./paths.mjs";
|
|
7
|
+
import {
|
|
8
|
+
mergeSettingsRuntimeMarkerIntoState,
|
|
9
|
+
settingsRuntimeMarkerFromObject,
|
|
10
|
+
settingsRuntimeMarkerFromState,
|
|
11
|
+
} from "./settings-runtime-state.mjs";
|
|
7
12
|
|
|
8
13
|
export function writeState(ctx, operation) {
|
|
9
14
|
const pkg = readCliPackageJson();
|
|
10
15
|
const git = gitStatus(ctx.repoRoot);
|
|
11
16
|
const settings = readJsonFileIfExists(path.join(ctx.agentDir, "settings.json")) || {};
|
|
17
|
+
const statePath = path.join(ctx.stateDir, "state.json");
|
|
18
|
+
const previous = readJsonFileIfExists(statePath) || {};
|
|
12
19
|
const state = {
|
|
13
20
|
schema: "pi67.state.v1",
|
|
14
21
|
updatedAt: new Date().toISOString(),
|
|
@@ -25,7 +32,12 @@ export function writeState(ctx, operation) {
|
|
|
25
32
|
lastProvider: settings.defaultProvider || "",
|
|
26
33
|
lastModel: settings.defaultModel || "",
|
|
27
34
|
};
|
|
35
|
+
const mergedState = mergeSettingsRuntimeMarkerIntoState(
|
|
36
|
+
state,
|
|
37
|
+
settingsRuntimeMarkerFromObject(settings) || settingsRuntimeMarkerFromState(previous),
|
|
38
|
+
state.updatedAt,
|
|
39
|
+
);
|
|
28
40
|
fs.mkdirSync(ctx.stateDir, { recursive: true });
|
|
29
|
-
writeJsonAtomic(
|
|
30
|
-
return
|
|
41
|
+
writeJsonAtomic(statePath, mergedState);
|
|
42
|
+
return mergedState;
|
|
31
43
|
}
|
package/src/lib/update-plan.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { readCliPackageJson, readTextIfExists } from "./paths.mjs";
|
|
|
9
9
|
import { npmLatestVersion } from "./npm-registry.mjs";
|
|
10
10
|
import { buildDistroManifest } from "./distro-manifest.mjs";
|
|
11
11
|
import { PRESERVED_RUNTIME_FILES } from "./update-safety.mjs";
|
|
12
|
+
import { settingsRuntimeMarkerFromObject } from "./settings-runtime-state.mjs";
|
|
12
13
|
|
|
13
14
|
export async function buildUpdatePlan(ctx, options = {}) {
|
|
14
15
|
const versionFile = path.join(ctx.repoRoot, "VERSION");
|
|
@@ -39,6 +40,7 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
39
40
|
currentVersion: pkg.version,
|
|
40
41
|
noRemote: options.noRemote,
|
|
41
42
|
});
|
|
43
|
+
const settingsRuntimeMarker = settingsRuntimeMarkerFromObject(settings);
|
|
42
44
|
const dirtyClass = classifyGitShort(git?.short || "");
|
|
43
45
|
const benignRuntime = classifyBenignRuntimeDiff(ctx, dirtyClass.preservedRuntime);
|
|
44
46
|
|
|
@@ -54,15 +56,14 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
54
56
|
} else if (git.dirty && dirtyClass.unsafeTracked.length > 0) {
|
|
55
57
|
recommendations.push("Resolve or commit local changes before pi-67 update.");
|
|
56
58
|
} else if (git.dirty && benignRuntime.benign) {
|
|
57
|
-
recommendations.push("No manual action required for benign settings runtime markers; pi-67
|
|
58
|
-
recommendations.push("Optional: normalize local settings runtime marker if you want a clean git status.");
|
|
59
|
+
recommendations.push("No manual action required for benign settings runtime markers; pi-67 migrates lastChangelogVersion to ignored state during update/repair.");
|
|
59
60
|
} else if (git.dirty) {
|
|
60
61
|
recommendations.push("No manual action required for user runtime config; pi-67 backs up/restores it during update.");
|
|
61
62
|
} else {
|
|
62
63
|
recommendations.push("Run: pi-67 update");
|
|
63
64
|
}
|
|
64
65
|
if (skills.summary.conflicts > 0) {
|
|
65
|
-
recommendations.push("Run: pi-67 skills inventory
|
|
66
|
+
recommendations.push("Run: pi-67 skills inventory to inspect preserved user-modified global skills.");
|
|
66
67
|
}
|
|
67
68
|
if (theme && !hasTheme(ctx, theme)) {
|
|
68
69
|
recommendations.push(`Current theme is missing: ${theme}. Run: pi-67 themes list`);
|
|
@@ -74,6 +75,7 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
74
75
|
ctx,
|
|
75
76
|
git,
|
|
76
77
|
benignRuntime,
|
|
78
|
+
settingsRuntimeMarker,
|
|
77
79
|
managerRegistry,
|
|
78
80
|
remote,
|
|
79
81
|
manifest,
|
|
@@ -122,6 +124,11 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
122
124
|
scripts: scriptStatus,
|
|
123
125
|
manifest: manifest.summary,
|
|
124
126
|
skills: skills.summary,
|
|
127
|
+
runtimeState: {
|
|
128
|
+
settingsLastChangelogVersion: settingsRuntimeMarker?.value || "",
|
|
129
|
+
storage: "~/.pi/pi67/state.json",
|
|
130
|
+
policy: "settings.json lastChangelogVersion is runtime-only and is normalized into ignored state on update/repair",
|
|
131
|
+
},
|
|
125
132
|
external,
|
|
126
133
|
actions: decisions.actions,
|
|
127
134
|
blocked: decisions.blocked,
|
|
@@ -139,6 +146,7 @@ export function buildPlanDecisions(context) {
|
|
|
139
146
|
];
|
|
140
147
|
const dirty = classifyGitShort(context.git?.short || "");
|
|
141
148
|
const benignRuntime = context.benignRuntime || { benign: false, reasons: [] };
|
|
149
|
+
const settingsRuntimeMarker = context.settingsRuntimeMarker || null;
|
|
142
150
|
|
|
143
151
|
if (context.managerRegistry.outdated) {
|
|
144
152
|
actions.push({
|
|
@@ -153,6 +161,19 @@ export function buildPlanDecisions(context) {
|
|
|
153
161
|
});
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
if (settingsRuntimeMarker?.value || benignRuntime.benign) {
|
|
165
|
+
actions.push({
|
|
166
|
+
id: "settings-runtime-state",
|
|
167
|
+
kind: "runtime-state",
|
|
168
|
+
operation: "migrate-lastChangelogVersion-to-ignored-state",
|
|
169
|
+
writes: ["~/.pi/pi67/state.json", "settings.json only when removing runtime-only lastChangelogVersion"],
|
|
170
|
+
preserves: ["settings.json.theme", "settings.json.defaultProvider", "settings.json.defaultModel", "settings.json.packages"],
|
|
171
|
+
risk: "low",
|
|
172
|
+
reason: "lastChangelogVersion is Pi runtime UI state, not pi-67 source configuration",
|
|
173
|
+
benign: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
156
177
|
if (!context.git?.isRepo) {
|
|
157
178
|
blocked.push({
|
|
158
179
|
id: "repo-root",
|
|
@@ -257,16 +278,16 @@ export function buildPlanDecisions(context) {
|
|
|
257
278
|
});
|
|
258
279
|
}
|
|
259
280
|
if (context.skills.summary.conflicts > 0) {
|
|
260
|
-
const conflictMessage = `${context.skills.summary.conflicts}
|
|
281
|
+
const conflictMessage = `${context.skills.summary.conflicts} preserved user-modified global skills differ from the bundled baseline`;
|
|
261
282
|
if (context.strictSharedSkills) {
|
|
262
283
|
blocked.push({
|
|
263
284
|
id: "shared-skills",
|
|
264
285
|
kind: "skill-pack",
|
|
265
|
-
reason: `${conflictMessage}; strict mode blocks instead of overwriting`,
|
|
286
|
+
reason: `${conflictMessage}; strict mode blocks instead of overwriting user-modified skills`,
|
|
266
287
|
recovery: "inspect with pi-67 skills inventory, then sync or resolve manually",
|
|
267
288
|
});
|
|
268
289
|
} else {
|
|
269
|
-
warnings.push(`${conflictMessage}; default update preserves
|
|
290
|
+
warnings.push(`${conflictMessage}; default update preserves them`);
|
|
270
291
|
}
|
|
271
292
|
}
|
|
272
293
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
if (args.includes("--clean")) {
|
|
9
|
+
const { stripSettingsRuntimeMarkerText } = await import("../lib/settings-runtime-clean.mjs");
|
|
10
|
+
const input = fs.readFileSync(0, "utf8");
|
|
11
|
+
process.stdout.write(stripSettingsRuntimeMarkerText(input));
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (args.includes("--migrate")) {
|
|
16
|
+
const { migrateSettingsRuntimeState } = await import("../lib/settings-runtime-state.mjs");
|
|
17
|
+
const ctx = {
|
|
18
|
+
agentDir: path.resolve(valueAfter("--agent-dir") || process.cwd()),
|
|
19
|
+
repoRoot: path.resolve(valueAfter("--repo-root") || process.cwd()),
|
|
20
|
+
stateDir: path.resolve(valueAfter("--state-dir") || path.join(os.homedir(), ".pi", "pi67")),
|
|
21
|
+
};
|
|
22
|
+
const result = migrateSettingsRuntimeState(ctx, {
|
|
23
|
+
normalizeSettingsJson: args.includes("--normalize"),
|
|
24
|
+
installGitFilter: args.includes("--install-git-filter"),
|
|
25
|
+
dryRun: args.includes("--dry-run"),
|
|
26
|
+
});
|
|
27
|
+
if (args.includes("--json")) {
|
|
28
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
29
|
+
} else {
|
|
30
|
+
printHuman(result);
|
|
31
|
+
}
|
|
32
|
+
process.exit(result.errors.length > 0 ? 1 : 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.stderr.write(`Usage:
|
|
36
|
+
settings-runtime-state-filter.mjs --clean
|
|
37
|
+
settings-runtime-state-filter.mjs --migrate --agent-dir DIR --repo-root DIR [--state-dir DIR] [--normalize] [--install-git-filter] [--json] [--dry-run]
|
|
38
|
+
`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
|
|
41
|
+
function valueAfter(name) {
|
|
42
|
+
const index = args.indexOf(name);
|
|
43
|
+
if (index === -1) return "";
|
|
44
|
+
return args[index + 1] || "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printHuman(result) {
|
|
48
|
+
if (result.markerFound) {
|
|
49
|
+
console.log(`PASS migrated settings.json lastChangelogVersion to ${result.statePath}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log("PASS settings.json runtime marker is already absent");
|
|
52
|
+
}
|
|
53
|
+
if (result.settingsNormalized) {
|
|
54
|
+
console.log("PASS normalized settings.json by removing runtime-only lastChangelogVersion");
|
|
55
|
+
}
|
|
56
|
+
if (result.gitFilterInstalled) {
|
|
57
|
+
console.log("PASS installed local git clean filter for settings.json runtime marker");
|
|
58
|
+
}
|
|
59
|
+
for (const item of result.skipped) {
|
|
60
|
+
console.log(`INFO ${item}`);
|
|
61
|
+
}
|
|
62
|
+
for (const item of result.errors) {
|
|
63
|
+
console.error(`FAIL ${item}`);
|
|
64
|
+
}
|
|
65
|
+
}
|