@bigking67/pi-67 0.10.4 → 0.10.6
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 +36 -0
- package/README.md +19 -6
- package/package.json +1 -1
- package/scripts/check.mjs +20 -0
- package/src/commands/doctor.mjs +9 -2
- package/src/commands/report.mjs +24 -3
- package/src/commands/update.mjs +6 -3
- package/src/commands/xtalpi.mjs +48 -0
- package/src/lib/distro-scripts.mjs +4 -0
- package/src/lib/update-plan.mjs +45 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.6]
|
|
4
|
+
|
|
5
|
+
- Adds runtime request retry to the canonical `xtalpi-pi-tools` provider path,
|
|
6
|
+
including attempt/retry/suppression telemetry in debug JSONL output.
|
|
7
|
+
- Adds final-answer compliance repair to Bash and PowerShell xtalpi smoke:
|
|
8
|
+
validated tool runs that only miss required final text get one `--no-tools`
|
|
9
|
+
repair pass instead of rerunning tools.
|
|
10
|
+
- Accepts `pi-67 doctor --no-skill-list` and raises doctor skill-list timeout
|
|
11
|
+
defaults to 60 seconds.
|
|
12
|
+
- Supports command-level `pi-67 report --json`, emitting either a dry-run JSON
|
|
13
|
+
object or the generated report JSON.
|
|
14
|
+
- Runs full `scripts/pi67-smoke.sh --ci` in the npm publish workflow before
|
|
15
|
+
pack/publish.
|
|
16
|
+
|
|
17
|
+
## [0.10.5]
|
|
18
|
+
|
|
19
|
+
- Uses `git fetch` plus `git merge --ff-only FETCH_HEAD` in Bash and
|
|
20
|
+
PowerShell updaters, allowing the updater to inspect incoming changed paths
|
|
21
|
+
before deciding whether dirty preserved runtime files need temporary cleanup.
|
|
22
|
+
- Keeps dirty runtime config in place without creating a backup when the remote
|
|
23
|
+
is already current or incoming changed paths do not touch those files.
|
|
24
|
+
- Reports `preserve-in-place-no-backup` from `pi-67 update --check` when a
|
|
25
|
+
dirty runtime marker is harmless and the remote already matches local HEAD.
|
|
26
|
+
- Updates docs to separate current `~/.pi/pi67/backups/` runtime backups from
|
|
27
|
+
legacy read-only `~/.pi/agent-backups/` conflict snapshots.
|
|
28
|
+
- Tightens release/smoke gates by checking README version drift, rejecting
|
|
29
|
+
simulated placeholder final answers, and requiring package metadata smoke
|
|
30
|
+
cases to include the real package version.
|
|
31
|
+
- Adds `pi-67 xtalpi run` and a Windows PowerShell xtalpi launcher. The stable
|
|
32
|
+
launcher defaults `PI_OBSERVATIONAL_MEMORY_PASSIVE=true` to keep post-final
|
|
33
|
+
observational-memory background writes from holding the main task lifecycle
|
|
34
|
+
open.
|
|
35
|
+
- Bounds `pi skill list` in doctor with POSIX
|
|
36
|
+
`--skill-list-timeout-seconds` and PowerShell `-SkillListTimeoutSeconds`.
|
|
37
|
+
- Removes legacy `xtalpi-compat` runtime path examples from bootstrap docs.
|
|
38
|
+
|
|
3
39
|
## [0.10.4]
|
|
4
40
|
|
|
5
41
|
- Makes all command-level `--help` paths side-effect free, including
|
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
|
|
@@ -77,13 +85,17 @@ Before a real `update` or `repair`, the npm manager builds the update plan,
|
|
|
77
85
|
blocks unsafe non-runtime dirty worktrees, and acquires
|
|
78
86
|
`~/.pi/pi67/locks/update.lock`. Runtime config backup/restore is delegated to
|
|
79
87
|
the Bash or PowerShell updater script only when an in-place checkout needs to
|
|
80
|
-
temporarily clear dirty preserved runtime files
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
temporarily clear dirty preserved runtime files. The updater fetches first,
|
|
89
|
+
compares incoming `HEAD..FETCH_HEAD` changed paths, and creates a runtime
|
|
90
|
+
snapshot only when the incoming update touches those dirty preserved files.
|
|
91
|
+
Those script-level snapshots live under
|
|
92
|
+
`~/.pi/pi67/backups/pre-update-runtime-*`.
|
|
93
|
+
This keeps `--help`, blocked update plans, already-up-to-date updates,
|
|
94
|
+
non-overlapping incoming updates, and the public `npx -y
|
|
83
95
|
@bigking67/pi-67@latest update --repair` orchestration path free of duplicate
|
|
84
|
-
manager-owned runtime backups. If
|
|
85
|
-
the script-level updater reuses it instead of
|
|
86
|
-
directory.
|
|
96
|
+
manager-owned runtime backups. If a backup is actually needed and an identical
|
|
97
|
+
runtime snapshot already exists, the script-level updater reuses it instead of
|
|
98
|
+
writing another timestamped directory.
|
|
87
99
|
|
|
88
100
|
Runtime backups are first-class CLI state:
|
|
89
101
|
|
|
@@ -139,6 +151,7 @@ pi-67 doctor
|
|
|
139
151
|
pi-67 smoke --quick
|
|
140
152
|
pi-67 status
|
|
141
153
|
pi-67 report
|
|
154
|
+
pi-67 report --json
|
|
142
155
|
pi-67 version
|
|
143
156
|
pi-67 xtalpi health
|
|
144
157
|
pi-67 xtalpi smoke --quick
|
package/package.json
CHANGED
package/scripts/check.mjs
CHANGED
|
@@ -59,6 +59,14 @@ function runArgsSelfTests() {
|
|
|
59
59
|
parseCommandOptions(["--help"], { bools: [] }).options.help,
|
|
60
60
|
"command option parser must accept --help for every command",
|
|
61
61
|
);
|
|
62
|
+
assert(
|
|
63
|
+
parseCommandOptions(["--no-skill-list"], { bools: ["no-skill-list"] }).options.noSkillList,
|
|
64
|
+
"command option parser must accept doctor --no-skill-list",
|
|
65
|
+
);
|
|
66
|
+
assert(
|
|
67
|
+
parseCommandOptions(["--json"], { bools: ["json"] }).options.json,
|
|
68
|
+
"command option parser must accept command-level --json",
|
|
69
|
+
);
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
function runCliHelpContractSelfTests() {
|
|
@@ -347,6 +355,18 @@ function runUpdatePlanSelfTests() {
|
|
|
347
355
|
"dirty runtime config alone must not block the distro update plan",
|
|
348
356
|
);
|
|
349
357
|
|
|
358
|
+
const dirtyRuntimeRemoteCurrent = decisionsFixture({
|
|
359
|
+
git: { dirty: true, short: " M settings.json", commit: "abcdef123456" },
|
|
360
|
+
remote: { ok: true, commit: "abcdef1234567890" },
|
|
361
|
+
});
|
|
362
|
+
const currentRemoteAction = buildPlanDecisions(dirtyRuntimeRemoteCurrent).actions.find((item) => item.id === "user-runtime-config");
|
|
363
|
+
assert(
|
|
364
|
+
currentRemoteAction?.operation === "preserve-in-place-no-backup" &&
|
|
365
|
+
currentRemoteAction.createsNewBackup === false &&
|
|
366
|
+
currentRemoteAction.writes.length === 0,
|
|
367
|
+
"dirty runtime config must not plan a new backup when the remote already matches local HEAD",
|
|
368
|
+
);
|
|
369
|
+
|
|
350
370
|
const dirtyReadme = decisionsFixture({
|
|
351
371
|
git: { dirty: true, short: " M README.md" },
|
|
352
372
|
});
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -4,8 +4,8 @@ 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"],
|
|
8
|
-
strings: ["mcp-timeout-ms"],
|
|
7
|
+
bools: ["json", "quiet", "dry-run", "deep-mcp", "no-skill-list", "strict-shared-skills"],
|
|
8
|
+
strings: ["mcp-timeout-ms", "skill-list-timeout-seconds"],
|
|
9
9
|
});
|
|
10
10
|
if (options.help) {
|
|
11
11
|
printDoctorHelp();
|
|
@@ -19,6 +19,10 @@ 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");
|
|
23
|
+
if (options.skillListTimeoutSeconds) {
|
|
24
|
+
args.push(isWindows() ? "-SkillListTimeoutSeconds" : "--skill-list-timeout-seconds", options.skillListTimeoutSeconds);
|
|
25
|
+
}
|
|
22
26
|
runDistroScript(ctx, { sh: "pi67-doctor.sh", ps1: "pi67-doctor.ps1" }, args, {
|
|
23
27
|
dryRun: ctx.dryRun || options.dryRun,
|
|
24
28
|
});
|
|
@@ -35,6 +39,9 @@ Options:
|
|
|
35
39
|
--quiet Reduce human output where supported.
|
|
36
40
|
--deep-mcp Run deeper MCP probes on POSIX platforms.
|
|
37
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.
|
|
43
|
+
--skill-list-timeout-seconds N
|
|
44
|
+
Timeout for pi skill list where enabled.
|
|
38
45
|
--strict-shared-skills Treat differing shared skills as blocking.
|
|
39
46
|
--dry-run Print the script invocation without running it.
|
|
40
47
|
|
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/update.mjs
CHANGED
|
@@ -135,7 +135,9 @@ function printPlan(plan) {
|
|
|
135
135
|
if (plan.actions?.length > 0) {
|
|
136
136
|
section("Planned safe actions");
|
|
137
137
|
for (const action of plan.actions) {
|
|
138
|
-
|
|
138
|
+
const writes = action.writes?.length ? action.writes.join(", ") : "none";
|
|
139
|
+
const suffix = action.backupCondition ? `; backup=${action.backupCondition}` : "";
|
|
140
|
+
info(`${action.id}: ${action.operation}; writes=${writes}; preserves=${action.preserves.join(", ")}${suffix}`);
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
if (plan.blocked?.length > 0) {
|
|
@@ -174,8 +176,9 @@ Options:
|
|
|
174
176
|
|
|
175
177
|
Safety:
|
|
176
178
|
Runtime config backup/restore is owned by the platform updater script when
|
|
177
|
-
preserved runtime files
|
|
178
|
-
never creates a runtime backup for --help
|
|
179
|
+
dirty preserved runtime files overlap incoming changed paths. The npm manager
|
|
180
|
+
owns the update lock and never creates a runtime backup for --help, a blocked
|
|
181
|
+
update plan, or an already-up-to-date update.
|
|
179
182
|
|
|
180
183
|
Examples:
|
|
181
184
|
pi-67 update --check
|
package/src/commands/xtalpi.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export async function xtalpiCommand(ctx, argv) {
|
|
|
16
16
|
if (sub === "trend") return trend(ctx, rest);
|
|
17
17
|
if (sub === "drift") return drift(ctx, rest);
|
|
18
18
|
if (sub === "stress") return stress(ctx, rest);
|
|
19
|
+
if (sub === "run") return run(ctx, rest);
|
|
19
20
|
throw new CliError(`unknown xtalpi command: ${sub}`, 2);
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -110,6 +111,47 @@ function stress(ctx, argv) {
|
|
|
110
111
|
return smoke(ctx, ["--profile", options.profile || "full-suite", ...(options.dryRun ? ["--dry-run"] : [])]);
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
function run(ctx, argv) {
|
|
115
|
+
const passthroughIndex = argv.indexOf("--");
|
|
116
|
+
const optionArgv = passthroughIndex === -1 ? argv : argv.slice(0, passthroughIndex);
|
|
117
|
+
const passthrough = passthroughIndex === -1 ? [] : argv.slice(passthroughIndex + 1);
|
|
118
|
+
const { options, positionals } = parseCommandOptions(optionArgv, {
|
|
119
|
+
strings: ["model", "provider"],
|
|
120
|
+
bools: ["dry-run", "no-passive-observational-memory"],
|
|
121
|
+
});
|
|
122
|
+
if (options.help) return printXtalpiHelp();
|
|
123
|
+
const provider = options.provider || "xtalpi-pi-tools";
|
|
124
|
+
const model = options.model || "deepseek-v4-pro";
|
|
125
|
+
const piArgs = [...positionals, ...passthrough];
|
|
126
|
+
const env = {};
|
|
127
|
+
if (options.noPassiveObservationalMemory) {
|
|
128
|
+
env.PI_OBSERVATIONAL_MEMORY_PASSIVE = "false";
|
|
129
|
+
}
|
|
130
|
+
if (isWindows()) {
|
|
131
|
+
const pwsh = findPowerShell();
|
|
132
|
+
if (!pwsh) throw new CliError("PowerShell executable not found");
|
|
133
|
+
const args = [
|
|
134
|
+
"-NoProfile",
|
|
135
|
+
"-ExecutionPolicy",
|
|
136
|
+
"Bypass",
|
|
137
|
+
"-File",
|
|
138
|
+
scriptPath(ctx, "pi67-xtalpi-pi-tools.ps1"),
|
|
139
|
+
"-Provider",
|
|
140
|
+
provider,
|
|
141
|
+
"-Model",
|
|
142
|
+
model,
|
|
143
|
+
...piArgs,
|
|
144
|
+
];
|
|
145
|
+
runCommand(pwsh, args, { cwd: ctx.repoRoot, dryRun: ctx.dryRun || options.dryRun, env });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
runCommand("bash", [scriptPath(ctx, "pi67-xtalpi-pi-tools.sh"), ...piArgs], {
|
|
149
|
+
cwd: ctx.repoRoot,
|
|
150
|
+
dryRun: ctx.dryRun || options.dryRun,
|
|
151
|
+
env: { PROVIDER: provider, MODEL: model, ...env },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
113
155
|
function profileFromOptions(options) {
|
|
114
156
|
if (options.profile) return options.profile;
|
|
115
157
|
if (options.extensionLowRisk) return "extension-low-risk";
|
|
@@ -129,11 +171,16 @@ Usage:
|
|
|
129
171
|
pi-67 xtalpi trend [--limit N] [--profile NAME] [--json] [--out-dir DIR]
|
|
130
172
|
pi-67 xtalpi drift [--limit N] [--run-kind LIST] [--json] [--out-dir DIR]
|
|
131
173
|
pi-67 xtalpi stress --until-done
|
|
174
|
+
pi-67 xtalpi run [--provider ID] [--model NAME] [--no-passive-observational-memory] [-- <pi args>]
|
|
132
175
|
|
|
133
176
|
Notes:
|
|
134
177
|
xtalpi-pi-tools treats xtalpi as plain chat-completions transport. Pi local
|
|
135
178
|
code owns tool protocol parsing, validation, repair, retry classification,
|
|
136
179
|
tool execution, and smoke gates.
|
|
180
|
+
xtalpi run uses the stable launcher and defaults
|
|
181
|
+
PI_OBSERVATIONAL_MEMORY_PASSIVE=true so post-final background memory writes
|
|
182
|
+
cannot hold the main task lifecycle open. Pass --no-passive-observational-memory
|
|
183
|
+
only when you explicitly want pi-observational-memory to record after final.
|
|
137
184
|
xtalpi drift defaults to --run-kind full-suite so targeted one-off smoke
|
|
138
185
|
artifacts do not create expected case-set drift noise.
|
|
139
186
|
|
|
@@ -144,5 +191,6 @@ Examples:
|
|
|
144
191
|
pi-67 xtalpi trend --json
|
|
145
192
|
pi-67 xtalpi drift --json
|
|
146
193
|
pi-67 xtalpi stress --until-done
|
|
194
|
+
pi-67 xtalpi run
|
|
147
195
|
`);
|
|
148
196
|
}
|
|
@@ -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
|
}
|
package/src/lib/update-plan.mjs
CHANGED
|
@@ -75,6 +75,7 @@ export async function buildUpdatePlan(ctx, options = {}) {
|
|
|
75
75
|
git,
|
|
76
76
|
benignRuntime,
|
|
77
77
|
managerRegistry,
|
|
78
|
+
remote,
|
|
78
79
|
manifest,
|
|
79
80
|
skills,
|
|
80
81
|
external,
|
|
@@ -167,23 +168,35 @@ export function buildPlanDecisions(context) {
|
|
|
167
168
|
recovery: "commit/stash intentional changes or rerun the script-level updater with an explicit dirty override",
|
|
168
169
|
});
|
|
169
170
|
} else if (context.git.dirty && dirty.preservedRuntime.length > 0) {
|
|
171
|
+
const remoteStatus = classifyIncomingRemoteStatus(context.git, context.remote);
|
|
172
|
+
const preserveInPlace = remoteStatus.upToDate;
|
|
170
173
|
actions.push({
|
|
171
174
|
id: "user-runtime-config",
|
|
172
175
|
kind: "runtime-config",
|
|
173
|
-
operation:
|
|
174
|
-
|
|
176
|
+
operation: preserveInPlace
|
|
177
|
+
? "preserve-in-place-no-backup"
|
|
178
|
+
: "conditional-backup-if-incoming-update-touches-runtime-config",
|
|
179
|
+
writes: preserveInPlace
|
|
180
|
+
? []
|
|
181
|
+
: ["~/.pi/pi67/backups/pre-update-runtime-* only if incoming update touches preserved runtime files"],
|
|
175
182
|
preserves: dirty.preservedRuntime,
|
|
176
183
|
risk: "low",
|
|
177
|
-
reason: benignRuntime
|
|
178
|
-
? `benign runtime marker only: ${benignRuntime.reasons.join("; ")}`
|
|
179
|
-
: "only user-owned runtime config files are dirty; update snapshots and restores them instead of overwriting",
|
|
184
|
+
reason: runtimeConfigActionReason({ benignRuntime, preserveInPlace, remoteStatus }),
|
|
180
185
|
benign: benignRuntime.benign,
|
|
181
186
|
benignReasons: benignRuntime.reasons,
|
|
187
|
+
createsNewBackup: !preserveInPlace,
|
|
188
|
+
backupCondition: preserveInPlace
|
|
189
|
+
? "none: remote already matches the local checkout"
|
|
190
|
+
: "only when fetched incoming changes overlap preserved runtime files",
|
|
182
191
|
});
|
|
183
192
|
warnings.push(
|
|
184
|
-
|
|
185
|
-
? `
|
|
186
|
-
:
|
|
193
|
+
preserveInPlace
|
|
194
|
+
? `dirty user runtime config will stay in place; current remote is already at the local commit: ${dirty.preservedRuntime.join(", ")}`
|
|
195
|
+
: (
|
|
196
|
+
benignRuntime.benign
|
|
197
|
+
? `benign user runtime marker will be preserved; backup is conditional on incoming path overlap: ${dirty.preservedRuntime.join(", ")}`
|
|
198
|
+
: `dirty user runtime config will be preserved; backup is conditional on incoming path overlap: ${dirty.preservedRuntime.join(", ")}`
|
|
199
|
+
),
|
|
187
200
|
);
|
|
188
201
|
}
|
|
189
202
|
if (context.git?.dirty && dirty.untracked.length > 0) {
|
|
@@ -289,6 +302,30 @@ export function buildPlanDecisions(context) {
|
|
|
289
302
|
return { actions, blocked, warnings };
|
|
290
303
|
}
|
|
291
304
|
|
|
305
|
+
function classifyIncomingRemoteStatus(git, remote) {
|
|
306
|
+
if (remote?.skipped) return { upToDate: false, known: false, reason: "remote check skipped" };
|
|
307
|
+
if (!remote?.commit || !git?.commit) return { upToDate: false, known: false, reason: remote?.message || "remote commit unknown" };
|
|
308
|
+
const local = String(git.commit);
|
|
309
|
+
const remoteCommit = String(remote.commit);
|
|
310
|
+
return {
|
|
311
|
+
upToDate: remoteCommit.startsWith(local),
|
|
312
|
+
known: true,
|
|
313
|
+
reason: remoteCommit.startsWith(local)
|
|
314
|
+
? "remote already matches local commit"
|
|
315
|
+
: "remote differs from local commit; updater will inspect changed paths after fetch",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function runtimeConfigActionReason({ benignRuntime, preserveInPlace, remoteStatus }) {
|
|
320
|
+
const prefix = benignRuntime.benign
|
|
321
|
+
? `benign runtime marker only: ${benignRuntime.reasons.join("; ")}`
|
|
322
|
+
: "only user-owned runtime config files are dirty";
|
|
323
|
+
if (preserveInPlace) {
|
|
324
|
+
return `${prefix}; ${remoteStatus.reason}; no runtime backup is needed`;
|
|
325
|
+
}
|
|
326
|
+
return `${prefix}; updater fetches first and creates a runtime backup only if incoming changes touch these preserved files`;
|
|
327
|
+
}
|
|
328
|
+
|
|
292
329
|
export function classifyGitShort(short) {
|
|
293
330
|
const preserved = new Set(PRESERVED_RUNTIME_FILES);
|
|
294
331
|
const result = {
|