skillwiki 0.10.41 → 0.10.43

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.
@@ -61,7 +61,8 @@ var ExitCode = {
61
61
  APPROVAL_INVALID: 56,
62
62
  APPROVAL_MISMATCH: 57,
63
63
  TARGET_DRIFT: 58,
64
- RECOVERY_EVIDENCE_MISSING: 59
64
+ RECOVERY_EVIDENCE_MISSING: 59,
65
+ SYNC_PARTIAL_DECLINE: 60
65
66
  };
66
67
 
67
68
  // ../shared/src/json-output.ts
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  renderRootIndex,
8
8
  writeRootIndexProjection
9
- } from "./chunk-QVO332CT.js";
9
+ } from "./chunk-NL2MUWNF.js";
10
10
  import {
11
11
  CONFIG_KEYS,
12
12
  acquireOwnedSyncLock,
@@ -34,15 +34,15 @@ import {
34
34
  snapshotterAliasForLocalHost,
35
35
  toUndirectedWeighted,
36
36
  writeDotenv
37
- } from "./chunk-XRBUABGE.js";
37
+ } from "./chunk-NPHRDXHD.js";
38
38
  import {
39
39
  atomicWriteText,
40
40
  prepareTypedPage
41
- } from "./chunk-IFMHQETQ.js";
41
+ } from "./chunk-CTN66CDX.js";
42
42
  import {
43
43
  eventPathFor,
44
44
  writeLogEvent
45
- } from "./chunk-E3JBAURS.js";
45
+ } from "./chunk-RPZIGMMH.js";
46
46
  import {
47
47
  CompoundSchema,
48
48
  ExitCode,
@@ -63,7 +63,7 @@ import {
63
63
  scanVault,
64
64
  splitFrontmatter,
65
65
  systemdPropertyFor
66
- } from "./chunk-BS556WUE.js";
66
+ } from "./chunk-APDMYPM6.js";
67
67
 
68
68
  // src/commands/log-append.ts
69
69
  import { readFile, stat } from "fs/promises";
@@ -3211,6 +3211,7 @@ var HYGIENE_COMMANDS = /* @__PURE__ */ new Set([
3211
3211
  "doctor",
3212
3212
  "health",
3213
3213
  "lint",
3214
+ "lint --fix",
3214
3215
  "status",
3215
3216
  "path",
3216
3217
  "fleet context",
@@ -4849,7 +4850,7 @@ async function runQuery(input) {
4849
4850
  }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
4850
4851
  let pendingSources;
4851
4852
  if (input.includePending) {
4852
- const { runSourcesPending } = await import("./sources-LIJAUXFU.js");
4853
+ const { runSourcesPending } = await import("./sources-Z4VMD2YO.js");
4853
4854
  const pending = await runSourcesPending({
4854
4855
  vault: input.vault,
4855
4856
  match: input.text,
@@ -8,7 +8,7 @@ import {
8
8
  ok,
9
9
  scanSensitiveContent,
10
10
  scanVault
11
- } from "./chunk-BS556WUE.js";
11
+ } from "./chunk-APDMYPM6.js";
12
12
 
13
13
  // src/utils/atomic-write.ts
14
14
  import { randomBytes } from "crypto";
@@ -8,7 +8,7 @@ import {
8
8
  readLogEvents,
9
9
  resolveExistingRegularFileInsideVault,
10
10
  writeLogEvent
11
- } from "./chunk-E3JBAURS.js";
11
+ } from "./chunk-RPZIGMMH.js";
12
12
  import {
13
13
  ExitCode,
14
14
  RawSourceSchema,
@@ -19,7 +19,7 @@ import {
19
19
  scanSensitiveContent,
20
20
  scanVault,
21
21
  splitFrontmatter
22
- } from "./chunk-BS556WUE.js";
22
+ } from "./chunk-APDMYPM6.js";
23
23
 
24
24
  // src/utils/source-lifecycle.ts
25
25
  import { basename } from "path";
@@ -22,12 +22,12 @@ import {
22
22
  runSyncLintDelta,
23
23
  runVaultSyncPullHelper,
24
24
  supersedeStaleReviewRequiredJournals
25
- } from "./chunk-XRBUABGE.js";
25
+ } from "./chunk-NPHRDXHD.js";
26
26
  import {
27
27
  ExitCode,
28
28
  err,
29
29
  ok
30
- } from "./chunk-BS556WUE.js";
30
+ } from "./chunk-APDMYPM6.js";
31
31
 
32
32
  // src/utils/managed-write-preflight.ts
33
33
  import { existsSync as existsSync3 } from "fs";
@@ -434,9 +434,15 @@ var STASH_AUDIT_CLASSIFICATIONS = [
434
434
  var STASH_AUDIT_FORMATS = ["wiki-sync", "vault-sync", "manual", "unknown"];
435
435
  var STASH_RECENT_MINUTES = 120;
436
436
  var MANAGED_WRITER_SPECS = [
437
- ["wiki-push", /(?:^|[\s/\\])wiki-push(?:\.[a-z0-9_-]+)?(?:\s|$)/i],
438
- ["rclone", /(?:^|[\s/\\])rclone(?:\.[a-z0-9_-]+)?(?:\s|$)/i],
439
- ["vault-sync", /(?:^|[\s/\\])vault-sync(?:\.[a-z0-9_-]+)?(?:\s|$)/i]
437
+ // Real runner only: wiki-push.sh on Unix, wiki-push/wiki-push.exe as a
438
+ // Windows image name. Bare text mentions never match.
439
+ ["wiki-push", /(?:^wiki-push(?:\.exe)?$)|(?:wiki-push\.sh(?:\s|$))/i],
440
+ // Write verbs only on Unix; Windows image name only (tasklist exposes
441
+ // no arguments). Read-only verbs and `rclone mount` (FUSE consumer)
442
+ // never match.
443
+ ["rclone", /(?:^rclone(?:\.exe)?$)|(?:\brclone(?:\.exe)?\s+(?:copy|sync|move|delete|purge|deletefile|copyto|moveto|dedupe|mkdir|rmdir|rmdirs|touch|settier|bisync|copyurl)(?:\s|$))/i],
444
+ // Writing helper scripts on Unix; Windows image name only.
445
+ ["vault-sync", /(?:^vault-sync(?:\.exe)?$)|(?:wiki-(?:snapshot|pull-with-auto-resolve|git-repair-v3)\.sh(?:\s|$))/i]
440
446
  ];
441
447
  var MANAGED_WRITER_KINDS = MANAGED_WRITER_SPECS.map(([kind]) => kind);
442
448
  function classifyStash(ageMinutes, sessionId, currentSession) {
@@ -3,11 +3,11 @@ import {
3
3
  ROOT_INDEX_SECTION_ORDER,
4
4
  atomicWriteText,
5
5
  buildRootIndexUniverse
6
- } from "./chunk-IFMHQETQ.js";
6
+ } from "./chunk-CTN66CDX.js";
7
7
  import {
8
8
  err,
9
9
  ok
10
- } from "./chunk-BS556WUE.js";
10
+ } from "./chunk-APDMYPM6.js";
11
11
 
12
12
  // src/utils/index-projection.ts
13
13
  import { readFile } from "fs/promises";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  atomicWriteText,
4
4
  buildRootIndexUniverse
5
- } from "./chunk-IFMHQETQ.js";
5
+ } from "./chunk-CTN66CDX.js";
6
6
  import {
7
7
  authorizeRawOperation,
8
8
  buildSourceReferenceIndex,
@@ -22,7 +22,7 @@ import {
22
22
  resolveExistingRegularFileInsideVault,
23
23
  stripFencedBlocks,
24
24
  writeLogEvent
25
- } from "./chunk-E3JBAURS.js";
25
+ } from "./chunk-RPZIGMMH.js";
26
26
  import {
27
27
  ExitCode,
28
28
  FleetManifestSchema,
@@ -40,7 +40,7 @@ import {
40
40
  scanVault,
41
41
  splitFrontmatter,
42
42
  vaultIoConcurrency
43
- } from "./chunk-BS556WUE.js";
43
+ } from "./chunk-APDMYPM6.js";
44
44
 
45
45
  // src/utils/sync-lock.ts
46
46
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
@@ -2996,8 +2996,10 @@ async function fixPathTooLong(input) {
2996
2996
  ];
2997
2997
  for (const f of fixed) hintLines.push(` ${f.from} -> ${f.to}`);
2998
2998
  for (const u of unresolved) hintLines.push(` unresolved: ${u}`);
2999
+ const nonRawUnresolved = unresolved.filter((p) => !p.startsWith("raw/"));
3000
+ const rawUnresolved = unresolved.filter((p) => p.startsWith("raw/"));
2999
3001
  return {
3000
- exitCode: unresolved.length > 0 ? ExitCode.LINT_HAS_ERRORS : ExitCode.OK,
3002
+ exitCode: nonRawUnresolved.length > 0 ? ExitCode.LINT_HAS_ERRORS : ExitCode.OK,
3001
3003
  result: ok({ fixed, unresolved, rewired, humanHint: hintLines.join("\n") })
3002
3004
  };
3003
3005
  }
@@ -4479,6 +4481,15 @@ ${newBody}`;
4479
4481
  if (remaining.length > 0) buckets.path_too_long = remaining;
4480
4482
  else delete buckets.path_too_long;
4481
4483
  }
4484
+ if (buckets.path_too_long) {
4485
+ const rawRemaining = buckets.path_too_long.filter((v) => v.relPath.startsWith("raw/"));
4486
+ if (rawRemaining.length > 0) {
4487
+ unresolved.push(...rawRemaining.map((v) => v.relPath));
4488
+ const nonRaw = buckets.path_too_long.filter((v) => !v.relPath.startsWith("raw/"));
4489
+ if (nonRaw.length > 0) buckets.path_too_long = nonRaw;
4490
+ else delete buckets.path_too_long;
4491
+ }
4492
+ }
4482
4493
  }
4483
4494
  if (shouldFix("frontmatter_yaml_invalid") && buckets.frontmatter_yaml_invalid) {
4484
4495
  const invalidItems = buckets.frontmatter_yaml_invalid;
@@ -6,7 +6,7 @@ import {
6
6
  readPage,
7
7
  scanSensitiveContent,
8
8
  splitFrontmatter
9
- } from "./chunk-BS556WUE.js";
9
+ } from "./chunk-APDMYPM6.js";
10
10
 
11
11
  // src/utils/log-events.ts
12
12
  import { mkdir, open, readFile, readdir } from "fs/promises";
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  scanConflictMarkerBlocksInText,
40
40
  snapshotterHealthChecks,
41
41
  upsertIndexEntry
42
- } from "./chunk-NDR2OY4K.js";
42
+ } from "./chunk-BUHQMIOD.js";
43
43
  import {
44
44
  normalizeDistTag,
45
45
  readCache,
@@ -52,7 +52,7 @@ import {
52
52
  UNMANAGED_START,
53
53
  renderRootIndex,
54
54
  writeRootIndexProjection
55
- } from "./chunk-QVO332CT.js";
55
+ } from "./chunk-NL2MUWNF.js";
56
56
  import {
57
57
  VAULT_COMMIT_PATHSPEC,
58
58
  runManagedWritePreflight,
@@ -64,7 +64,7 @@ import {
64
64
  runSyncStatus,
65
65
  runSyncUnlock,
66
66
  stageVaultContentChanges
67
- } from "./chunk-LACQJ6RD.js";
67
+ } from "./chunk-G5M65BHV.js";
68
68
  import {
69
69
  FLEET_REL_PATH,
70
70
  acquireOwnedSyncLock,
@@ -127,12 +127,12 @@ import {
127
127
  supersedeStaleReviewRequiredJournals,
128
128
  taxonomyCommentForPage,
129
129
  writeDotenv
130
- } from "./chunk-XRBUABGE.js";
130
+ } from "./chunk-NPHRDXHD.js";
131
131
  import {
132
132
  assertTargetInsideVault,
133
133
  atomicWriteText,
134
134
  prepareTypedPage
135
- } from "./chunk-IFMHQETQ.js";
135
+ } from "./chunk-CTN66CDX.js";
136
136
  import {
137
137
  applySourceDisposition,
138
138
  decodeSourceActionApproval,
@@ -140,7 +140,7 @@ import {
140
140
  inventorySources,
141
141
  planSourceDisposition,
142
142
  runSourcesPending
143
- } from "./chunk-JUUZJJIF.js";
143
+ } from "./chunk-DHGHISVY.js";
144
144
  import {
145
145
  authorizeRawOperation,
146
146
  buildSourceReferenceIndex,
@@ -156,7 +156,7 @@ import {
156
156
  resolveExistingRegularFileInsideVault,
157
157
  validateLogEvent,
158
158
  writeLogEvent
159
- } from "./chunk-E3JBAURS.js";
159
+ } from "./chunk-RPZIGMMH.js";
160
160
  import {
161
161
  ExitCode,
162
162
  MetaSchema,
@@ -173,7 +173,7 @@ import {
173
173
  scanSensitiveContent,
174
174
  scanVault,
175
175
  splitFrontmatter
176
- } from "./chunk-BS556WUE.js";
176
+ } from "./chunk-APDMYPM6.js";
177
177
 
178
178
  // src/cli.ts
179
179
  import { join as join38 } from "path";
@@ -1355,7 +1355,8 @@ async function runDerivedConflictResolution(input) {
1355
1355
  }
1356
1356
  const classes = paths.map((p) => ({ path: p, klass: classifyDerivedPath(p) }));
1357
1357
  const unknown = classes.filter((c) => c.klass === "unknown").map((c) => c.path);
1358
- if (unknown.length > 0) {
1358
+ const derived = classes.filter((c) => c.klass !== "unknown");
1359
+ if (unknown.length > 0 && derived.length === 0) {
1359
1360
  return {
1360
1361
  exitCode: ExitCode.SYNC_PULL_FAILED,
1361
1362
  result: ok({
@@ -1367,6 +1368,53 @@ async function runDerivedConflictResolution(input) {
1367
1368
  })
1368
1369
  };
1369
1370
  }
1371
+ if (unknown.length > 0 && derived.length > 0) {
1372
+ const snapshots2 = /* @__PURE__ */ new Map();
1373
+ for (const { path } of derived) {
1374
+ snapshots2.set(path, {
1375
+ stage2: stageOid(vault, 2, path),
1376
+ stage3: stageOid(vault, 3, path)
1377
+ });
1378
+ }
1379
+ const staged2 = [];
1380
+ try {
1381
+ for (const { path, klass } of derived) {
1382
+ const resolved = await resolveOne(vault, path, klass);
1383
+ if (!resolved.ok) throw new Error(`${path}: ${resolved.error}`);
1384
+ if (hasMarkers(path, resolved.data)) {
1385
+ throw new Error(`${path}: conflict markers remain after resolve`);
1386
+ }
1387
+ writeFileSync2(join8(vault, path), resolved.data, "utf8");
1388
+ execFileSync2("git", ["add", "--", path], { cwd: vault, stdio: ["pipe", "pipe", "pipe"] });
1389
+ staged2.push(path);
1390
+ }
1391
+ return {
1392
+ exitCode: ExitCode.SYNC_PARTIAL_DECLINE,
1393
+ result: ok({
1394
+ resolved: false,
1395
+ resolved_paths: staged2,
1396
+ unknown_paths: unknown,
1397
+ rolled_back: false,
1398
+ humanHint: `partial-decline: resolved ${staged2.length} derived path(s), ${unknown.length} unknown path(s) need manual review: ${unknown.join(", ")}`
1399
+ })
1400
+ };
1401
+ } catch (error) {
1402
+ for (const path of staged2) {
1403
+ const snap = snapshots2.get(path);
1404
+ if (snap) rollbackPath(vault, path, snap.stage2, snap.stage3);
1405
+ }
1406
+ return {
1407
+ exitCode: ExitCode.SYNC_PULL_FAILED,
1408
+ result: ok({
1409
+ resolved: false,
1410
+ resolved_paths: [],
1411
+ unknown_paths: unknown,
1412
+ rolled_back: true,
1413
+ humanHint: `partial-decline resolve failed: ${String(error)}`
1414
+ })
1415
+ };
1416
+ }
1417
+ }
1370
1418
  const snapshots = /* @__PURE__ */ new Map();
1371
1419
  for (const { path } of classes) {
1372
1420
  snapshots.set(path, {
@@ -3429,7 +3477,7 @@ ${fmRewritten}
3429
3477
  }
3430
3478
  let indexUpdated = false;
3431
3479
  if (!isRaw) {
3432
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ZXFN5DT4.js");
3480
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-RWF5M7U7.js");
3433
3481
  const before = await readFile7(join18(input.vault, "index.md"), "utf8").catch(() => "");
3434
3482
  const fullTarget = relPath.replace(/\.md$/, "");
3435
3483
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -3549,7 +3597,7 @@ async function runRemove(input) {
3549
3597
  if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
3550
3598
  const { readFile: readFile20 } = await import("fs/promises");
3551
3599
  const { join: pathJoin } = await import("path");
3552
- const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ZXFN5DT4.js");
3600
+ const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-RWF5M7U7.js");
3553
3601
  const before = await readFile20(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
3554
3602
  const fullTarget = relPath.replace(/\.md$/, "");
3555
3603
  const bare = fullTarget.split("/").pop() ?? fullTarget;
@@ -9494,7 +9542,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
9494
9542
  if (dirty) {
9495
9543
  return emit(dirty, void 0, { postCommit: false });
9496
9544
  }
9497
- const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-ANQPMOF4.js");
9545
+ const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-ZDBELXAM.js");
9498
9546
  const run = await runManagedWriteTransaction2({
9499
9547
  vault,
9500
9548
  command,
@@ -4,9 +4,9 @@ import {
4
4
  UNMANAGED_START,
5
5
  renderRootIndex,
6
6
  writeRootIndexProjection
7
- } from "./chunk-QVO332CT.js";
8
- import "./chunk-IFMHQETQ.js";
9
- import "./chunk-BS556WUE.js";
7
+ } from "./chunk-NL2MUWNF.js";
8
+ import "./chunk-CTN66CDX.js";
9
+ import "./chunk-APDMYPM6.js";
10
10
  export {
11
11
  UNMANAGED_END,
12
12
  UNMANAGED_START,
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  runManagedWritePreflight,
4
4
  runManagedWriteTransaction
5
- } from "./chunk-LACQJ6RD.js";
6
- import "./chunk-XRBUABGE.js";
7
- import "./chunk-IFMHQETQ.js";
8
- import "./chunk-E3JBAURS.js";
9
- import "./chunk-BS556WUE.js";
5
+ } from "./chunk-G5M65BHV.js";
6
+ import "./chunk-NPHRDXHD.js";
7
+ import "./chunk-CTN66CDX.js";
8
+ import "./chunk-RPZIGMMH.js";
9
+ import "./chunk-APDMYPM6.js";
10
10
  export {
11
11
  runManagedWritePreflight,
12
12
  runManagedWriteTransaction
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-NDR2OY4K.js";
4
+ } from "./chunk-BUHQMIOD.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
- import "./chunk-QVO332CT.js";
7
- import "./chunk-XRBUABGE.js";
8
- import "./chunk-IFMHQETQ.js";
9
- import "./chunk-E3JBAURS.js";
10
- import "./chunk-BS556WUE.js";
6
+ import "./chunk-NL2MUWNF.js";
7
+ import "./chunk-NPHRDXHD.js";
8
+ import "./chunk-CTN66CDX.js";
9
+ import "./chunk-RPZIGMMH.js";
10
+ import "./chunk-APDMYPM6.js";
11
11
 
12
12
  // src/mcp-entry.ts
13
13
  runSkillwikiMcpStdio().catch((error) => {
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runSourcesPending
4
+ } from "./chunk-DHGHISVY.js";
5
+ import "./chunk-RPZIGMMH.js";
6
+ import "./chunk-APDMYPM6.js";
7
+ export {
8
+ runSourcesPending
9
+ };
@@ -199,28 +199,64 @@ verify_no_tracked_conflict_markers() {
199
199
  # Composite derived-artifact resolver (log, root index, project index, taxonomy).
200
200
  # Delegates the complete conflict set to skillwiki sync resolve-derived; no
201
201
  # blanket checkout --ours for archive/snapshot mixed sets.
202
+ #
203
+ # M4: Returns 0 on full resolve (all paths resolved), 60 on partial-decline
204
+ # (derived subset resolved, unknown paths remain), 1 on failure/decline.
205
+ # On partial-decline (60), sets RESOLVED_PATHS to the newline-separated list
206
+ # of paths the resolver actually rewrote (for the inventory skip set).
202
207
  try_auto_resolve_derived_conflicts() {
203
208
  local op_id="$1"
204
209
  local cli_ts="${SCRIPT_DIR}/../../cli/src/cli.ts"
205
210
  local cmd_rc=1
211
+ local resolver_out
212
+ RESOLVED_PATHS=""
206
213
  if [ -z "$op_id" ]; then
207
214
  return 1
208
215
  fi
209
216
  # Prefer monorepo CLI when present (packaged install uses adjacent dist later).
210
217
  if [ -f "$cli_ts" ] && command -v npx >/dev/null 2>&1; then
211
- if VAULT_SYNC_OPERATION_ID="$op_id" \
212
- npx --yes tsx "$cli_ts" sync resolve-derived "$WIKI_DIR" --operation-id "$op_id" >>"$LOG_FILE" 2>&1; then
218
+ resolver_out="$(VAULT_SYNC_OPERATION_ID="$op_id" \
219
+ npx --yes tsx "$cli_ts" sync resolve-derived "$WIKI_DIR" --operation-id "$op_id" 2>>"$LOG_FILE")"
220
+ cmd_rc=$?
221
+ printf '%s\n' "$resolver_out" >>"$LOG_FILE"
222
+ if [ "$cmd_rc" -eq 0 ]; then
213
223
  log "AUTO-RESOLVE composite derived artifacts op=$op_id"
214
224
  return 0
225
+ elif [ "$cmd_rc" -eq 60 ]; then
226
+ RESOLVED_PATHS="$(printf '%s\n' "$resolver_out" | python3 -c '
227
+ import json,sys
228
+ try:
229
+ d=json.load(sys.stdin)
230
+ data=d.get("data") or {}
231
+ for p in (data.get("resolved_paths") or []):
232
+ print(p)
233
+ except: pass
234
+ ' 2>/dev/null)"
235
+ log "PARTIAL-RESOLVE derived artifacts op=$op_id unknowns remain"
236
+ return 60
215
237
  fi
216
- cmd_rc=$?
217
238
  log "WARN monorepo resolve-derived failed rc=$cmd_rc op=$op_id"
218
239
  fi
219
240
  if command -v skillwiki >/dev/null 2>&1; then
220
- if VAULT_SYNC_OPERATION_ID="$op_id" \
221
- skillwiki sync resolve-derived "$WIKI_DIR" --operation-id "$op_id" >>"$LOG_FILE" 2>&1; then
241
+ resolver_out="$(VAULT_SYNC_OPERATION_ID="$op_id" \
242
+ skillwiki sync resolve-derived "$WIKI_DIR" --operation-id "$op_id" 2>>"$LOG_FILE")"
243
+ cmd_rc=$?
244
+ printf '%s\n' "$resolver_out" >>"$LOG_FILE"
245
+ if [ "$cmd_rc" -eq 0 ]; then
222
246
  log "AUTO-RESOLVE composite derived artifacts op=$op_id (skillwiki)"
223
247
  return 0
248
+ elif [ "$cmd_rc" -eq 60 ]; then
249
+ RESOLVED_PATHS="$(printf '%s\n' "$resolver_out" | python3 -c '
250
+ import json,sys
251
+ try:
252
+ d=json.load(sys.stdin)
253
+ data=d.get("data") or {}
254
+ for p in (data.get("resolved_paths") or []):
255
+ print(p)
256
+ except: pass
257
+ ' 2>/dev/null)"
258
+ log "PARTIAL-RESOLVE derived artifacts op=$op_id unknowns remain (skillwiki)"
259
+ return 60
224
260
  fi
225
261
  fi
226
262
  return 1
@@ -588,6 +624,26 @@ while [ $REBASE_RC -ne 0 ]; do
588
624
  continue
589
625
  fi
590
626
 
627
+ # M4: Partial-decline — derived subset resolved, unknown paths remain.
628
+ # The unknown paths must go through the manual-conflict path (retry/handoff).
629
+ # The derived paths are already git-added; rebase --continue will fail on
630
+ # the remaining unknowns, so we go straight to handle_manual_conflict.
631
+ if [ "$?" -eq 60 ]; then
632
+ local unknown_conflicts
633
+ unknown_conflicts="$(git diff --name-only --diff-filter=U 2>/dev/null)"
634
+ if [ -n "$unknown_conflicts" ]; then
635
+ STOPPED_SHA=$(cat "$WIKI_DIR/.git/rebase-merge/stopped-sha" 2>/dev/null || echo "")
636
+ if [ -z "$STOPPED_SHA" ]; then
637
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "missing-stopped-sha-partial"
638
+ log "WARN cannot determine stopped-sha — surfacing partial-decline conflicts"
639
+ exit 1
640
+ fi
641
+ COMMIT_MSG=$(git log --format=%s -1 "$STOPPED_SHA" 2>/dev/null || echo "")
642
+ handle_manual_conflict "$COMMIT_MSG" "$unknown_conflicts"
643
+ continue
644
+ fi
645
+ fi
646
+
591
647
  # Composite resolver declined (unknown paths or failure). Manual path:
592
648
  # one owned stale-target retry, else handoff — no blanket --ours.
593
649
  STOPPED_SHA=$(cat "$WIKI_DIR/.git/rebase-merge/stopped-sha" 2>/dev/null || echo "")
@@ -634,6 +690,33 @@ if [ -n "$OWNED_STASH_OID" ]; then
634
690
 
635
691
  vault_sync_op_stash_drop_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE" || true
636
692
  log "STASH apply derived conflicts auto-resolved"
693
+ elif [ -n "$CONFLICTS" ] && [ "$?" -eq 60 ]; then
694
+ # M4: Partial-decline during stash-apply. The resolver resolved the
695
+ # derived subset and returned resolved_paths. Unknown paths remain
696
+ # conflicted — surface as review-required.
697
+ if [ -n "$(git diff --name-only --diff-filter=U 2>/dev/null)" ]; then
698
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "stash-restore-partial-decline"
699
+ log "FAIL stash apply partial-decline left unknown conflicts"
700
+ exit 1
701
+ fi
702
+ # Merge resolved_paths (from the resolver) into the skip set so the
703
+ # inventory byte-equality check does not fail on content-transformed
704
+ # derived paths. The conflict set alone is insufficient because the
705
+ # resolver regenerates derived files that may not have been in the
706
+ # stash-apply conflict set (derived-conflicts.ts:100-134).
707
+ SKIP_CONTENT="$(printf '%s\n' $CONFLICTS)"
708
+ if [ -n "${RESOLVED_PATHS:-}" ]; then
709
+ SKIP_CONTENT="${SKIP_CONTENT}
710
+ ${RESOLVED_PATHS}"
711
+ fi
712
+ if ! vault_sync_op_verify_inventory "$WIKI_DIR" "$OP_ID" "$OWNED_STASH_OID" "$SKIP_CONTENT" 2>>"$LOG_FILE"; then
713
+ vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "inventory-verify-failed-partial"
714
+ log "FAIL inventory verification after partial-decline oid=$OWNED_STASH_OID"
715
+ exit 1
716
+ fi
717
+
718
+ vault_sync_op_stash_drop_owned "$WIKI_DIR" "$OWNED_STASH_OID" 2>>"$LOG_FILE" || true
719
+ log "STASH apply partial-decline resolved derived subset"
637
720
  else
638
721
  vault_sync_op_mark_review_required "$WIKI_DIR" "$OP_ID" "semantic-conflict"
639
722
  log "FAIL stash apply after rebase oid=$OWNED_STASH_OID"
@@ -44,11 +44,72 @@ PUSH_DEDUP_DISABLE="${WIKI_PUSH_FAIL_DEDUP_DISABLE:-0}"
44
44
  PUSH_DEDUP_COOLDOWN_SECONDS="${WIKI_PUSH_FAIL_DEDUP_COOLDOWN_SECONDS:-900}"
45
45
  PUSH_DEDUP_STATE_FILE="$(platform_cache_dir)/wiki-push-fail-dedup.state"
46
46
  PUSH_DEDUP_PAUSE_MARKER="$(platform_cache_dir)/wiki-push.paused"
47
+ # M3: durable terminal-state file for health checks. Survives log rotation
48
+ # (1 MB x5) and is readable by status.sh / doctor at any time. Records the
49
+ # outcome of the most recent push attempt: OK or refused+reason.
50
+ PUSH_RESULT_FILE="$(platform_cache_dir)/wiki-push-result.state"
47
51
 
48
- mkdir -p "$(dirname "$LOCK_FILE")" "$(dirname "$LOG_FILE")"
52
+ mkdir -p "$(dirname "$LOCK_FILE")" "$(dirname "$LOG_FILE")" "$(dirname "$PUSH_RESULT_FILE")"
49
53
 
50
54
  log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"; }
51
55
 
56
+ # M3: Write a machine-readable push result record. This is the durable
57
+ # terminal-state file that status.sh / doctor read to determine push health.
58
+ # Format: key=value lines (same .env convention as operation journals).
59
+ # result=ok|refused
60
+ # reason=<refuse class, or empty for ok>
61
+ # timestamp=<ISO 8601 UTC>
62
+ # duration=<seconds, or empty>
63
+ write_push_result() {
64
+ local result="$1" reason="$2" duration="${3:-}"
65
+ local ts
66
+ ts="$(date -u +%FT%TZ)"
67
+ {
68
+ printf 'result=%s\n' "$result"
69
+ printf 'reason=%s\n' "$reason"
70
+ printf 'timestamp=%s\n' "$ts"
71
+ if [ -n "$duration" ]; then
72
+ printf 'duration=%s\n' "$duration"
73
+ fi
74
+ } > "$PUSH_RESULT_FILE" 2>/dev/null || true
75
+ }
76
+
77
+ # M3: Unified refuse handler. Logs the FAIL line, writes the terminal-state
78
+ # record, runs the P1 dedup cooldown check (wired to ALL refuse classes, not
79
+ # just conflict markers), and exits non-zero so the scheduler/monitoring can
80
+ # see the refusal. The P1 dedup suppresses duplicate FAIL lines within the
81
+ # cooldown window and escalates to a pause marker when the cooldown expires.
82
+ #
83
+ # Exit codes: 1 for guard refusals (the launchd plist uses StartInterval, not
84
+ # KeepAlive, so non-zero exits are pure signal -- no backoff/disable loops).
85
+ refuse_push() {
86
+ local reason="$1" message="$2"
87
+ local dedup_state
88
+ dedup_state="$(push_dedup_cooldown_check)"
89
+ case "$dedup_state" in
90
+ first)
91
+ log "FAIL $message"
92
+ push_dedup_touch_state
93
+ ;;
94
+ expired)
95
+ log "FAIL $message"
96
+ log "cooldown expired — pausing push (refusal persists past ${PUSH_DEDUP_COOLDOWN_SECONDS}s)"
97
+ push_dedup_write_pause_marker
98
+ push_dedup_touch_state
99
+ ;;
100
+ cooldown)
101
+ # Within cooldown: suppress the duplicate FAIL line. The first
102
+ # FAIL line in the incident is already logged; subsequent
103
+ # invocations within the cooldown are silent.
104
+ ;;
105
+ disabled)
106
+ log "FAIL $message"
107
+ ;;
108
+ esac
109
+ write_push_result "refused" "$reason"
110
+ exit 1
111
+ }
112
+
52
113
  # Terminal-state check: if the pause marker exists from a prior incident,
53
114
  # the launchd job is already unloaded. Honor the pause — log once and exit
54
115
  # without re-scanning or re-attempting the push. The user clears this by
@@ -117,40 +178,10 @@ conflict_marker_guard() {
117
178
  local findings
118
179
  findings="$(mktemp)" || { log "FAIL could not create conflict-marker scan temp file"; return 1; }
119
180
  if ! vault_sync_scan_conflict_markers "$WIKI_DIR" "$findings"; then
120
- local dedup_state
121
- dedup_state="$(push_dedup_cooldown_check)"
122
- case "$dedup_state" in
123
- first)
124
- # First detection: log the FAIL line, mark the state file so
125
- # subsequent invocations within the cooldown window are silent.
126
- log "FAIL conflict marker blocks present; refusing S3 push"
127
- vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
128
- push_dedup_touch_state
129
- ;;
130
- expired)
131
- # Past the cooldown: log the FAIL line + escalation notice,
132
- # write the pause marker, refresh the state file so the next
133
- # cooldown starts from this moment.
134
- log "FAIL conflict marker blocks present; refusing S3 push"
135
- vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
136
- log "cooldown expired — pausing push (markers persist past ${PUSH_DEDUP_COOLDOWN_SECONDS}s)"
137
- push_dedup_write_pause_marker
138
- push_dedup_touch_state
139
- ;;
140
- cooldown)
141
- # Within cooldown: suppress the duplicate FAIL line. The first
142
- # FAIL line in the incident (state=first) is already logged;
143
- # subsequent invocations within the cooldown are silent.
144
- ;;
145
- disabled)
146
- # Dedup is off: log the FAIL line (legacy behavior), do not
147
- # touch any state file or pause marker.
148
- log "FAIL conflict marker blocks present; refusing S3 push"
149
- vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
150
- ;;
151
- esac
181
+ # M3: delegate to the unified refuse_push handler (dedup + result file + non-zero exit).
182
+ vault_sync_log_conflict_marker_findings "$findings" "$LOG_FILE"
152
183
  rm -f "$findings"
153
- return 1
184
+ refuse_push "conflict-markers" "conflict marker blocks present; refusing S3 push"
154
185
  fi
155
186
  push_dedup_clear_state
156
187
  rm -f "$findings"
@@ -327,19 +358,16 @@ fi
327
358
 
328
359
  # Guard: wiki dir present.
329
360
  if [ ! -d "$WIKI_DIR" ]; then
330
- log "ERROR: WIKI_DIR not found at $WIKI_DIR"
331
- exit 0
361
+ refuse_push "wiki-dir-missing" "WIKI_DIR not found at $WIKI_DIR"
332
362
  fi
333
363
 
334
364
  # Guard: filter file present (it's not strictly required, but missing it would push .git/).
335
365
  if [ ! -f "$FILTERS" ]; then
336
- log "ERROR: filter file missing at $FILTERS refusing to push without exclusions"
337
- exit 0
366
+ refuse_push "filter-file-missing" "filter file missing at $FILTERS -- refusing to push without exclusions"
338
367
  fi
339
368
  # Projection-authority transport: non-authority leaves must not push root aggregates.
340
369
  if ! grep -qxF -- '- /index.md' "$FILTERS" || ! grep -qxF -- '- /log.md' "$FILTERS"; then
341
- log "ERROR: filter file missing root aggregate exclusions (- /index.md and - /log.md)"
342
- exit 1
370
+ refuse_push "filter-root-aggregate-missing" "filter file missing root aggregate exclusions (- /index.md and - /log.md)"
343
371
  fi
344
372
 
345
373
  # Acquire lockfile (non-blocking).
@@ -355,9 +383,8 @@ fi
355
383
  START=$(date +%s)
356
384
 
357
385
  if ! CASE_CONFLICTS=$(cd "$WIKI_DIR" && git_case_conflicts); then
358
- log "ERROR: case-only path collision detected — refusing rclone push"
359
386
  printf '%s\n' "$CASE_CONFLICTS" >>"$LOG_FILE"
360
- exit 0
387
+ refuse_push "case-collision" "case-only path collision detected -- refusing rclone push"
361
388
  fi
362
389
 
363
390
  # Fix Windows-hostile markdown paths before publishing to S3.
@@ -368,17 +395,13 @@ if command -v skillwiki >/dev/null 2>&1; then
368
395
  PATH_FIX_RC=$?
369
396
  printf '%s\n' "$PATH_FIX_OUTPUT" >>"$LOG_FILE"
370
397
  if [ "$PATH_FIX_RC" -ne 0 ]; then
371
- log "ERROR: path_too_long fix failed exit=$PATH_FIX_RC refusing rclone push"
372
- exit 0
398
+ refuse_push "path-fix-failed" "path_too_long fix failed exit=$PATH_FIX_RC -- refusing rclone push"
373
399
  fi
374
400
  else
375
- log "ERROR: skillwiki CLI not found refusing push because path_too_long guard cannot run"
376
- exit 0
401
+ refuse_push "skillwiki-not-found" "skillwiki CLI not found -- refusing push because path_too_long guard cannot run"
377
402
  fi
378
403
 
379
- if ! conflict_marker_guard; then
380
- exit 1
381
- fi
404
+ conflict_marker_guard
382
405
 
383
406
  # Lint delta gate: block S3 publication only on newly introduced lint errors.
384
407
  # Inherited debt is logged. Malformed/missing delta evidence fails closed.
@@ -402,20 +425,17 @@ except Exception:
402
425
  print("DELTA_RESOLVED=-1")
403
426
  ' 2>/dev/null)" || { DELTA_FULL=-1; DELTA_BASE=-1; DELTA_NEW=-1; DELTA_RESOLVED=-1; }
404
427
  if [ "${DELTA_NEW:--1}" = "-1" ] || [ "${DELTA_FULL:--1}" = "-1" ]; then
405
- log "FAIL lint-delta evidence missing or malformed refusing S3 push"
406
- exit 1
428
+ refuse_push "lint-delta-malformed" "lint-delta evidence missing or malformed -- refusing S3 push"
407
429
  fi
408
430
  log "LINT-DELTA full=$DELTA_FULL base=$DELTA_BASE new=$DELTA_NEW resolved=$DELTA_RESOLVED"
409
431
  if [ "$DELTA_NEW" -gt 0 ]; then
410
- log "FAIL lint-delta new_errors=$DELTA_NEW blocks S3 push (full=$DELTA_FULL)"
411
- exit 1
432
+ refuse_push "lint-delta-new-errors" "lint-delta new_errors=$DELTA_NEW blocks S3 push (full=$DELTA_FULL)"
412
433
  fi
413
434
  if [ "$DELTA_FULL" -gt 0 ]; then
414
435
  log "WARN inherited lint debt full_errors=$DELTA_FULL (new_errors=0) — push allowed"
415
436
  fi
416
437
  else
417
- log "FAIL skillwiki CLI not found lint-delta gate cannot run; refusing S3 push"
418
- exit 1
438
+ refuse_push "skillwiki-not-found" "skillwiki CLI not found -- lint-delta gate cannot run; refusing S3 push"
419
439
  fi
420
440
 
421
441
  # rclone copy (NOT sync) → never bulk-deletes on remote.
@@ -449,9 +469,11 @@ if [ "$RC" -eq 0 ]; then
449
469
  else
450
470
  log "OK push (no changes) duration=${DURATION}s"
451
471
  fi
472
+ write_push_result "ok" "" "$DURATION"
452
473
  else
453
474
  log "FAIL rclone exit=$RC duration=${DURATION}s"
454
475
  printf '%s\n' "$OUTPUT" >>"$LOG_FILE"
476
+ write_push_result "refused" "rclone-failed" "$DURATION"
455
477
  fi
456
478
 
457
479
  if [ "$RC" -eq 0 ]; then
@@ -468,4 +490,10 @@ if [ "$RC" -eq 0 ]; then
468
490
  rm -f "$PUSH_DEDUP_PAUSE_MARKER" 2>/dev/null || true
469
491
  fi
470
492
 
493
+ # M3: exit non-zero on rclone failure so the scheduler/monitoring sees it.
494
+ # The launchd plist uses StartInterval (not KeepAlive), so non-zero exits
495
+ # are pure signal -- no backoff/disable loops.
496
+ if [ "$RC" -ne 0 ]; then
497
+ exit 1
498
+ fi
471
499
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.41",
3
+ "version": "0.10.43",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.41",
3
+ "version": "0.10.43",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.41",
3
+ "version": "0.10.43",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 19 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.41",
3
+ "version": "0.10.43",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- runSourcesPending
4
- } from "./chunk-JUUZJJIF.js";
5
- import "./chunk-E3JBAURS.js";
6
- import "./chunk-BS556WUE.js";
7
- export {
8
- runSourcesPending
9
- };