@bigking67/pi-67 0.10.4 → 0.10.5

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.5]
4
+
5
+ - Uses `git fetch` plus `git merge --ff-only FETCH_HEAD` in Bash and
6
+ PowerShell updaters, allowing the updater to inspect incoming changed paths
7
+ before deciding whether dirty preserved runtime files need temporary cleanup.
8
+ - Keeps dirty runtime config in place without creating a backup when the remote
9
+ is already current or incoming changed paths do not touch those files.
10
+ - Reports `preserve-in-place-no-backup` from `pi-67 update --check` when a
11
+ dirty runtime marker is harmless and the remote already matches local HEAD.
12
+ - Updates docs to separate current `~/.pi/pi67/backups/` runtime backups from
13
+ legacy read-only `~/.pi/agent-backups/` conflict snapshots.
14
+ - Tightens release/smoke gates by checking README version drift, rejecting
15
+ simulated placeholder final answers, and requiring package metadata smoke
16
+ cases to include the real package version.
17
+ - Adds `pi-67 xtalpi run` and a Windows PowerShell xtalpi launcher. The stable
18
+ launcher defaults `PI_OBSERVATIONAL_MEMORY_PASSIVE=true` to keep post-final
19
+ observational-memory background writes from holding the main task lifecycle
20
+ open.
21
+ - Bounds `pi skill list` in doctor with POSIX
22
+ `--skill-list-timeout-seconds` and PowerShell `-SkillListTimeoutSeconds`.
23
+ - Removes legacy `xtalpi-compat` runtime path examples from bootstrap docs.
24
+
3
25
  ## [0.10.4]
4
26
 
5
27
  - Makes all command-level `--help` paths side-effect free, including
package/README.md CHANGED
@@ -77,13 +77,17 @@ Before a real `update` or `repair`, the npm manager builds the update plan,
77
77
  blocks unsafe non-runtime dirty worktrees, and acquires
78
78
  `~/.pi/pi67/locks/update.lock`. Runtime config backup/restore is delegated to
79
79
  the Bash or PowerShell updater script only when an in-place checkout needs to
80
- temporarily clear dirty preserved runtime files for `git pull --ff-only`. Those
81
- script-level snapshots live under `~/.pi/pi67/backups/pre-update-runtime-*`.
82
- This keeps `--help`, blocked update plans, and the public `npx -y
80
+ temporarily clear dirty preserved runtime files. The updater fetches first,
81
+ compares incoming `HEAD..FETCH_HEAD` changed paths, and creates a runtime
82
+ snapshot only when the incoming update touches those dirty preserved files.
83
+ Those script-level snapshots live under
84
+ `~/.pi/pi67/backups/pre-update-runtime-*`.
85
+ This keeps `--help`, blocked update plans, already-up-to-date updates,
86
+ non-overlapping incoming updates, and the public `npx -y
83
87
  @bigking67/pi-67@latest update --repair` orchestration path free of duplicate
84
- manager-owned runtime backups. If an identical runtime snapshot already exists,
85
- the script-level updater reuses it instead of writing another timestamped
86
- directory.
88
+ manager-owned runtime backups. If a backup is actually needed and an identical
89
+ runtime snapshot already exists, the script-level updater reuses it instead of
90
+ writing another timestamped directory.
87
91
 
88
92
  Runtime backups are first-class CLI state:
89
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigking67/pi-67",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Cross-platform manager CLI for the pi-67 Pi Coding Agent distribution.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/check.mjs CHANGED
@@ -347,6 +347,18 @@ function runUpdatePlanSelfTests() {
347
347
  "dirty runtime config alone must not block the distro update plan",
348
348
  );
349
349
 
350
+ const dirtyRuntimeRemoteCurrent = decisionsFixture({
351
+ git: { dirty: true, short: " M settings.json", commit: "abcdef123456" },
352
+ remote: { ok: true, commit: "abcdef1234567890" },
353
+ });
354
+ const currentRemoteAction = buildPlanDecisions(dirtyRuntimeRemoteCurrent).actions.find((item) => item.id === "user-runtime-config");
355
+ assert(
356
+ currentRemoteAction?.operation === "preserve-in-place-no-backup" &&
357
+ currentRemoteAction.createsNewBackup === false &&
358
+ currentRemoteAction.writes.length === 0,
359
+ "dirty runtime config must not plan a new backup when the remote already matches local HEAD",
360
+ );
361
+
350
362
  const dirtyReadme = decisionsFixture({
351
363
  git: { dirty: true, short: " M README.md" },
352
364
  });
@@ -5,7 +5,7 @@ import { isWindows } from "../lib/platform.mjs";
5
5
  export async function doctorCommand(ctx, argv) {
6
6
  const { options } = parseCommandOptions(argv, {
7
7
  bools: ["json", "quiet", "dry-run", "deep-mcp", "strict-shared-skills"],
8
- strings: ["mcp-timeout-ms"],
8
+ strings: ["mcp-timeout-ms", "skill-list-timeout-seconds"],
9
9
  });
10
10
  if (options.help) {
11
11
  printDoctorHelp();
@@ -19,6 +19,9 @@ 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 (options.skillListTimeoutSeconds) {
23
+ args.push(isWindows() ? "-SkillListTimeoutSeconds" : "--skill-list-timeout-seconds", options.skillListTimeoutSeconds);
24
+ }
22
25
  runDistroScript(ctx, { sh: "pi67-doctor.sh", ps1: "pi67-doctor.ps1" }, args, {
23
26
  dryRun: ctx.dryRun || options.dryRun,
24
27
  });
@@ -35,6 +38,8 @@ Options:
35
38
  --quiet Reduce human output where supported.
36
39
  --deep-mcp Run deeper MCP probes on POSIX platforms.
37
40
  --mcp-timeout-ms N Timeout for deep MCP probes on POSIX platforms.
41
+ --skill-list-timeout-seconds N
42
+ Timeout for pi skill list on POSIX platforms.
38
43
  --strict-shared-skills Treat differing shared skills as blocking.
39
44
  --dry-run Print the script invocation without running it.
40
45
 
@@ -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
- info(`${action.id}: ${action.operation}; writes=${action.writes.join(", ")}; preserves=${action.preserves.join(", ")}`);
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 are dirty. The npm manager owns the update lock and
178
- never creates a runtime backup for --help or a blocked update plan.
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
@@ -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
  }
@@ -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: "backup-and-restore-during-update",
174
- writes: ["~/.pi/pi67/backups/pre-update-runtime-*"],
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.benign
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
- benignRuntime.benign
185
- ? `benign user runtime marker will be preserved across update: ${dirty.preservedRuntime.join(", ")}`
186
- : `dirty user runtime config will be preserved across update: ${dirty.preservedRuntime.join(", ")}`,
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 = {