snipara-companion 3.0.3 → 3.0.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/dist/index.d.ts CHANGED
@@ -1971,6 +1971,7 @@ interface MemoryAuditResult {
1971
1971
  version: "snipara.memory_audit.v1";
1972
1972
  generatedAt: string;
1973
1973
  scope?: MemoryScope;
1974
+ summary?: MemoryAuditSummary;
1974
1975
  health?: Record<string, unknown>;
1975
1976
  cleanCandidates?: Record<string, unknown>;
1976
1977
  compactDryRun?: Record<string, unknown>;
@@ -1979,6 +1980,16 @@ interface MemoryAuditResult {
1979
1980
  message: string;
1980
1981
  }>;
1981
1982
  }
1983
+ interface MemoryAuditSummary {
1984
+ totalScanned?: number;
1985
+ activeCount?: number;
1986
+ autoCompactThreshold?: number;
1987
+ autoCompactWouldTrigger?: boolean;
1988
+ cleanupCandidateCounts: Record<string, number>;
1989
+ compactDryRunMutated?: boolean;
1990
+ compactDryRunPlannedActions?: number;
1991
+ recommendedActions: string[];
1992
+ }
1982
1993
  declare function memoryHealthCommand(options: MemoryHealthCommandOptions): Promise<void>;
1983
1994
  declare function memoryCleanCandidatesCommand(options: MemoryCleanCandidatesCommandOptions): Promise<void>;
1984
1995
  declare function memoryCompactCommand(options: MemoryCompactCommandOptions): Promise<void>;
@@ -2407,6 +2418,7 @@ interface TeamSyncSummary {
2407
2418
  handoffCount: number;
2408
2419
  files: string[];
2409
2420
  staleWorkExplanation: TeamSyncStaleWorkExplanation;
2421
+ hygieneActions: TeamSyncHygieneAction[];
2410
2422
  latestActiveWork?: TeamSyncWorkRecord;
2411
2423
  latestStaleWork?: TeamSyncWorkRecord;
2412
2424
  latestCompletedWork?: TeamSyncWorkRecord;
@@ -2423,6 +2435,12 @@ interface TeamSyncStaleWorkExplanation {
2423
2435
  latestStaleWorkAgeHours?: number;
2424
2436
  message: string;
2425
2437
  }
2438
+ interface TeamSyncHygieneAction {
2439
+ kind: "sweep-preview" | "sweep-archive" | "complete-work" | "handoff";
2440
+ command: string;
2441
+ reason: string;
2442
+ mutates: boolean;
2443
+ }
2426
2444
  interface TeamSyncCommandOptions {
2427
2445
  id?: string;
2428
2446
  summary?: string;
package/dist/index.js CHANGED
@@ -8826,6 +8826,16 @@ function isRecord2(value) {
8826
8826
  function numberValue(value) {
8827
8827
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8828
8828
  }
8829
+ function numberLikeValue(value) {
8830
+ if (typeof value === "number" && Number.isFinite(value)) {
8831
+ return value;
8832
+ }
8833
+ if (typeof value === "string" && value.trim()) {
8834
+ const parsed = Number(value);
8835
+ return Number.isFinite(parsed) ? parsed : void 0;
8836
+ }
8837
+ return void 0;
8838
+ }
8829
8839
  function preview(value, maxLength = 180) {
8830
8840
  const text = typeof value === "string" ? value : value === void 0 || value === null ? "" : JSON.stringify(value);
8831
8841
  return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
@@ -8989,6 +8999,78 @@ function printCompactDryRun(result) {
8989
8999
  }
8990
9000
  }
8991
9001
  }
9002
+ function buildMemoryAuditSummary(audit) {
9003
+ const health = isRecord2(audit.health) ? audit.health : {};
9004
+ const healthCounts = isRecord2(health.counts) ? health.counts : {};
9005
+ const healthByStatus = isRecord2(healthCounts.by_status) ? healthCounts.by_status : {};
9006
+ const autoCompact = isRecord2(health.auto_compact) ? health.auto_compact : {};
9007
+ const cleanCandidates = isRecord2(audit.cleanCandidates) ? audit.cleanCandidates : {};
9008
+ const candidateCounts = isRecord2(cleanCandidates.counts) ? cleanCandidates.counts : {};
9009
+ const compactDryRun = isRecord2(audit.compactDryRun) ? audit.compactDryRun : {};
9010
+ const cleanupCandidateCounts = Object.fromEntries(
9011
+ Object.entries(candidateCounts).map(([key, value]) => [key, numberLikeValue(value)]).filter((entry) => entry[1] !== void 0)
9012
+ );
9013
+ const totalScanned = numberLikeValue(health.total_scanned ?? cleanCandidates.total_scanned);
9014
+ const activeCount = numberLikeValue(healthByStatus.active);
9015
+ const autoCompactThreshold = numberLikeValue(autoCompact.threshold);
9016
+ const autoCompactWouldTrigger = autoCompact.would_trigger_by_count === true;
9017
+ const compactDryRunPlannedActions = numberLikeValue(
9018
+ compactDryRun.planned_actions ?? compactDryRun.plannedActions
9019
+ );
9020
+ const recommendedActions = [];
9021
+ const scope = audit.scope ?? "project";
9022
+ if (autoCompactWouldTrigger || activeCount !== void 0 && autoCompactThreshold !== void 0 && activeCount > autoCompactThreshold) {
9023
+ recommendedActions.push(
9024
+ `snipara-companion memory clean-candidates --scope ${scope} --limit-per-bucket 5`
9025
+ );
9026
+ recommendedActions.push(
9027
+ `snipara-companion memory compact --scope ${scope} --archive-older-than-days 30`
9028
+ );
9029
+ }
9030
+ if (Object.values(cleanupCandidateCounts).some((count) => count > 0)) {
9031
+ recommendedActions.push(
9032
+ 'snipara-companion memory-guard check --intent "apply memory cleanup" --destructive --strict'
9033
+ );
9034
+ }
9035
+ return {
9036
+ ...totalScanned !== void 0 ? { totalScanned } : {},
9037
+ ...activeCount !== void 0 ? { activeCount } : {},
9038
+ ...autoCompactThreshold !== void 0 ? { autoCompactThreshold } : {},
9039
+ ...Object.keys(autoCompact).length > 0 ? { autoCompactWouldTrigger } : {},
9040
+ cleanupCandidateCounts,
9041
+ ...typeof compactDryRun.mutated === "boolean" ? { compactDryRunMutated: compactDryRun.mutated } : {},
9042
+ ...compactDryRunPlannedActions !== void 0 ? { compactDryRunPlannedActions } : {},
9043
+ recommendedActions: uniqueStrings3(recommendedActions)
9044
+ };
9045
+ }
9046
+ function uniqueStrings3(values) {
9047
+ return Array.from(new Set(values));
9048
+ }
9049
+ function printMemoryAuditSummary(summary) {
9050
+ console.log(import_chalk3.default.bold("Memory Hygiene Summary"));
9051
+ if (summary.totalScanned !== void 0) {
9052
+ console.log(`Scanned: ${summary.totalScanned}`);
9053
+ }
9054
+ if (summary.activeCount !== void 0) {
9055
+ console.log(`Active: ${summary.activeCount}`);
9056
+ }
9057
+ if (summary.autoCompactThreshold !== void 0) {
9058
+ console.log(
9059
+ `Auto-compaction: threshold ${summary.autoCompactThreshold} | would trigger: ${summary.autoCompactWouldTrigger ? "yes" : "no"}`
9060
+ );
9061
+ }
9062
+ if (Object.keys(summary.cleanupCandidateCounts).length > 0) {
9063
+ console.log(
9064
+ `Cleanup candidates: ${Object.entries(summary.cleanupCandidateCounts).map(([key, value]) => `${key}: ${value}`).join(" | ")}`
9065
+ );
9066
+ }
9067
+ if (summary.recommendedActions.length > 0) {
9068
+ console.log("Recommended actions:");
9069
+ for (const action of summary.recommendedActions) {
9070
+ console.log(`- ${action}`);
9071
+ }
9072
+ }
9073
+ }
8992
9074
  function printMemoryInvalidate(result) {
8993
9075
  console.log(import_chalk3.default.bold("Memory Invalidated"));
8994
9076
  console.log(`Memory: ${result.memory_id}`);
@@ -9119,6 +9201,7 @@ async function buildMemoryAudit(options) {
9119
9201
  message: error instanceof Error ? error.message : String(error)
9120
9202
  });
9121
9203
  }
9204
+ result.summary = buildMemoryAuditSummary(result);
9122
9205
  return result;
9123
9206
  }
9124
9207
  async function memoryAuditCommand(options) {
@@ -9132,6 +9215,10 @@ async function memoryAuditCommand(options) {
9132
9215
  console.log(`Scope: ${audit.scope}`);
9133
9216
  }
9134
9217
  console.log("");
9218
+ if (audit.summary) {
9219
+ printMemoryAuditSummary(audit.summary);
9220
+ console.log("");
9221
+ }
9135
9222
  if (audit.health) {
9136
9223
  printMemoryHealth(audit.health);
9137
9224
  console.log("");
@@ -9163,7 +9250,7 @@ async function memoryAuditCommand(options) {
9163
9250
  var fs12 = __toESM(require("fs"));
9164
9251
  var path11 = __toESM(require("path"));
9165
9252
  var import_node_child_process2 = require("child_process");
9166
- function uniqueStrings3(values) {
9253
+ function uniqueStrings4(values) {
9167
9254
  return Array.from(new Set((values ?? []).map((value) => value.trim()).filter(Boolean)));
9168
9255
  }
9169
9256
  function slugify(value, fallback) {
@@ -9171,7 +9258,7 @@ function slugify(value, fallback) {
9171
9258
  return slug2 || fallback;
9172
9259
  }
9173
9260
  function keywordHints(value) {
9174
- return uniqueStrings3(
9261
+ return uniqueStrings4(
9175
9262
  value.split(/[^A-Za-z0-9_.:/-]+/).map((part) => part.trim()).filter((part) => part.length >= 4).slice(0, 8)
9176
9263
  );
9177
9264
  }
@@ -9217,17 +9304,17 @@ function expectedItems(section, values, files) {
9217
9304
  }
9218
9305
  function buildEvalCaseArtifact(options = {}) {
9219
9306
  const rootDir = path11.resolve(options.dir ?? process.cwd());
9220
- const files = uniqueStrings3(options.files);
9221
- const commandsRun = uniqueStrings3(options.commandRun);
9307
+ const files = uniqueStrings4(options.files);
9308
+ const commandsRun = uniqueStrings4(options.commandRun);
9222
9309
  const summary = options.summary?.trim() || "Snipara companion local workflow evaluation";
9223
9310
  const id = options.id?.trim() || slugify(summary, "snipara-companion-eval");
9224
9311
  const expected = {};
9225
9312
  const sectionInputs = {
9226
- context: uniqueStrings3(options.context),
9227
- decisions: uniqueStrings3(options.decision),
9228
- impact: uniqueStrings3(options.impact),
9229
- verification: uniqueStrings3(options.verification),
9230
- continuity: uniqueStrings3(options.continuity)
9313
+ context: uniqueStrings4(options.context),
9314
+ decisions: uniqueStrings4(options.decision),
9315
+ impact: uniqueStrings4(options.impact),
9316
+ verification: uniqueStrings4(options.verification),
9317
+ continuity: uniqueStrings4(options.continuity)
9231
9318
  };
9232
9319
  for (const [section, values] of Object.entries(sectionInputs)) {
9233
9320
  if (values.length > 0) {
@@ -9370,7 +9457,7 @@ function detectRuntimeEnvironment(cwd = process.cwd(), env = process.env) {
9370
9457
  const workspaceRoot = findWorkspaceRoot(cwd);
9371
9458
  const mcpConfigPaths = findRuntimeMcpConfigPaths(cwd, workspaceRoot);
9372
9459
  const sandboxCli = detectSandboxCli();
9373
- const orchestratorCli = detectOrchestratorCli();
9460
+ const orchestratorCli = detectOrchestratorCli(cwd, workspaceRoot);
9374
9461
  const parsedVersion = parseRuntimeVersion(sandboxCli.version);
9375
9462
  const providerKeys = detectProviderKeys(cwd, workspaceRoot, env);
9376
9463
  return {
@@ -9501,6 +9588,21 @@ function getCommandVersion(command) {
9501
9588
  return void 0;
9502
9589
  }
9503
9590
  }
9591
+ function executableExists(filePath) {
9592
+ try {
9593
+ const stat = fs13.statSync(filePath);
9594
+ if (!stat.isFile()) {
9595
+ return false;
9596
+ }
9597
+ if (process.platform === "win32") {
9598
+ return true;
9599
+ }
9600
+ fs13.accessSync(filePath, fs13.constants.X_OK);
9601
+ return true;
9602
+ } catch {
9603
+ return false;
9604
+ }
9605
+ }
9504
9606
  function detectSandboxCli() {
9505
9607
  for (const command of SANDBOX_COMMANDS) {
9506
9608
  const version = getCommandVersion(command);
@@ -9514,7 +9616,7 @@ function detectSandboxCli() {
9514
9616
  }
9515
9617
  return {};
9516
9618
  }
9517
- function detectOrchestratorCli() {
9619
+ function detectOrchestratorCli(cwd, workspaceRoot) {
9518
9620
  const version = getCommandVersion(ORCHESTRATOR_COMMAND);
9519
9621
  if (version || commandExists(ORCHESTRATOR_COMMAND)) {
9520
9622
  return {
@@ -9522,8 +9624,33 @@ function detectOrchestratorCli() {
9522
9624
  version
9523
9625
  };
9524
9626
  }
9627
+ for (const candidate of findWorkspaceExecutableCandidates(
9628
+ ORCHESTRATOR_COMMAND,
9629
+ cwd,
9630
+ workspaceRoot
9631
+ )) {
9632
+ if (!executableExists(candidate)) {
9633
+ continue;
9634
+ }
9635
+ return {
9636
+ command: candidate,
9637
+ version: getCommandVersion(candidate)
9638
+ };
9639
+ }
9525
9640
  return {};
9526
9641
  }
9642
+ function findWorkspaceExecutableCandidates(command, cwd, workspaceRoot) {
9643
+ const executableName = process.platform === "win32" ? `${command}.exe` : command;
9644
+ const roots = Array.from(
9645
+ new Set([workspaceRoot, path12.resolve(cwd)].filter((value) => Boolean(value)))
9646
+ );
9647
+ return roots.flatMap((root) => [
9648
+ path12.join(root, ".venv", "bin", executableName),
9649
+ path12.join(root, "venv", "bin", executableName),
9650
+ path12.join(root, "packages", "agentic-orchestrator", ".venv", "bin", executableName),
9651
+ path12.join(root, "apps", "mcp-server", ".venv", "bin", executableName)
9652
+ ]);
9653
+ }
9527
9654
  function parseRuntimeVersion(version) {
9528
9655
  if (!version) {
9529
9656
  return {};
@@ -10590,7 +10717,7 @@ function normalizeStringList(values) {
10590
10717
  }
10591
10718
 
10592
10719
  // src/commands/journal.ts
10593
- function uniqueStrings4(values) {
10720
+ function uniqueStrings5(values) {
10594
10721
  if (!values) {
10595
10722
  return [];
10596
10723
  }
@@ -10618,7 +10745,7 @@ function buildJournalCheckpointEntry(payload) {
10618
10745
  payload.actor ? `Actor: ${payload.actor}` : null,
10619
10746
  payload.attention ? `Attention: ${payload.attention}` : null,
10620
10747
  payload.next ? `Next: ${payload.next}` : null,
10621
- uniqueStrings4(payload.files).length > 0 ? `Files: ${uniqueStrings4(payload.files).join(", ")}` : null
10748
+ uniqueStrings5(payload.files).length > 0 ? `Files: ${uniqueStrings5(payload.files).join(", ")}` : null
10622
10749
  ].filter((line) => Boolean(line));
10623
10750
  return {
10624
10751
  text: lines.join("\n"),
@@ -10703,6 +10830,13 @@ function buildTeamSyncSummary(state, since, now = /* @__PURE__ */ new Date()) {
10703
10830
  files.add(file);
10704
10831
  }
10705
10832
  }
10833
+ const staleWorkExplanation = buildStaleWorkExplanation(
10834
+ work,
10835
+ staleWork,
10836
+ completedWork,
10837
+ archivedWork,
10838
+ now
10839
+ );
10706
10840
  return {
10707
10841
  activeWorkCount: activeWork.length,
10708
10842
  staleWorkCount: staleWork.length,
@@ -10710,13 +10844,8 @@ function buildTeamSyncSummary(state, since, now = /* @__PURE__ */ new Date()) {
10710
10844
  archivedWorkCount: archivedWork.length,
10711
10845
  handoffCount: handoffs.length,
10712
10846
  files: Array.from(files).sort(),
10713
- staleWorkExplanation: buildStaleWorkExplanation(
10714
- work,
10715
- staleWork,
10716
- completedWork,
10717
- archivedWork,
10718
- now
10719
- ),
10847
+ staleWorkExplanation,
10848
+ hygieneActions: buildTeamSyncHygieneActions(staleWork, staleWorkExplanation),
10720
10849
  latestActiveWork: activeWork[activeWork.length - 1],
10721
10850
  latestStaleWork: staleWork[staleWork.length - 1],
10722
10851
  latestCompletedWork: completedWork[completedWork.length - 1],
@@ -10724,6 +10853,42 @@ function buildTeamSyncSummary(state, since, now = /* @__PURE__ */ new Date()) {
10724
10853
  latestHandoff: handoffs[handoffs.length - 1]
10725
10854
  };
10726
10855
  }
10856
+ function buildTeamSyncHygieneActions(staleWork, explanation) {
10857
+ if (staleWork.length === 0) {
10858
+ return [];
10859
+ }
10860
+ const actions = [];
10861
+ if (explanation.autoArchivableCount > 0) {
10862
+ actions.push({
10863
+ kind: "sweep-preview",
10864
+ command: `snipara-companion team-sync sweep --days ${explanation.autoArchiveAfterDays} --dry-run`,
10865
+ reason: `${explanation.autoArchivableCount} stale active item(s) are old enough to archive.`,
10866
+ mutates: false
10867
+ });
10868
+ actions.push({
10869
+ kind: "sweep-archive",
10870
+ command: `snipara-companion team-sync sweep --days ${explanation.autoArchiveAfterDays}`,
10871
+ reason: "Archive stale local Team Sync work without deleting history.",
10872
+ mutates: true
10873
+ });
10874
+ }
10875
+ const latest = staleWork[staleWork.length - 1];
10876
+ if (latest) {
10877
+ actions.push({
10878
+ kind: "complete-work",
10879
+ command: `snipara-companion team-sync complete-work --id ${latest.id} --next '<completion reason>'`,
10880
+ reason: "Close the latest stale item if the work is actually done.",
10881
+ mutates: true
10882
+ });
10883
+ actions.push({
10884
+ kind: "handoff",
10885
+ command: "snipara-companion team-sync handoff --summary '<current state>' --next '<next action>' --attention watch",
10886
+ reason: "Refresh continuity if the stale work is still active.",
10887
+ mutates: true
10888
+ });
10889
+ }
10890
+ return actions;
10891
+ }
10727
10892
  function buildStaleWorkExplanation(work, staleWork, completedWork, archivedWork, now) {
10728
10893
  const autoArchivableCount = work.filter(
10729
10894
  (item) => item.status === "active" && now.getTime() - getWorkTimestamp(item) > TEAM_SYNC_AUTO_ARCHIVE_WORK_MS
@@ -10776,8 +10941,12 @@ function completeTeamSyncWorkFromEvidence(state, options = {}) {
10776
10941
  const workflowGoalText = normalizeCompletionText(options.workflowGoal);
10777
10942
  const summaryText = normalizeCompletionText(options.summary);
10778
10943
  const evidenceFiles = normalizeFiles(options.files);
10779
- const candidates = state.work.filter(
10780
- (item) => item.status === "active" && matchesCompletionEvidence(item, { workflowGoalText, summaryText, files: evidenceFiles })
10944
+ const activeCandidates = state.work.filter((item) => item.status === "active");
10945
+ const directCandidates = activeCandidates.filter(
10946
+ (item) => matchesDirectCompletionEvidence(item, { workflowGoalText, summaryText })
10947
+ );
10948
+ const candidates = directCandidates.length > 0 ? directCandidates : activeCandidates.filter(
10949
+ (item) => matchesCompletionEvidence(item, { workflowGoalText, summaryText, files: evidenceFiles })
10781
10950
  );
10782
10951
  if (!options.dryRun) {
10783
10952
  for (const item of candidates) {
@@ -11574,6 +11743,12 @@ function printTeamSyncResult(payload, json) {
11574
11743
  if (summary.staleWorkCount > 0) {
11575
11744
  console.log(`Stale detail: ${summary.staleWorkExplanation.message}`);
11576
11745
  }
11746
+ if (summary.hygieneActions.length > 0) {
11747
+ console.log("Hygiene actions:");
11748
+ for (const action2 of summary.hygieneActions.slice(0, 4)) {
11749
+ console.log(`- ${action2.command} (${action2.reason})`);
11750
+ }
11751
+ }
11577
11752
  if (summary.files.length > 0) {
11578
11753
  console.log(`Files: ${summary.files.slice(0, 8).join(", ")}`);
11579
11754
  }
@@ -12053,6 +12228,9 @@ function resolveRootDir(dir) {
12053
12228
  return path15.resolve(dir ?? process.cwd());
12054
12229
  }
12055
12230
  function matchesCompletionEvidence(item, evidence) {
12231
+ if (matchesDirectCompletionEvidence(item, evidence)) {
12232
+ return true;
12233
+ }
12056
12234
  const itemText = normalizeCompletionText(item.summary);
12057
12235
  if (!itemText) {
12058
12236
  return false;
@@ -12060,16 +12238,23 @@ function matchesCompletionEvidence(item, evidence) {
12060
12238
  const evidenceTexts = [evidence.workflowGoalText, evidence.summaryText].filter(
12061
12239
  (value) => Boolean(value)
12062
12240
  );
12063
- if (evidenceTexts.some((text) => completionTextsMatch(itemText, text))) {
12064
- return true;
12065
- }
12066
- if (evidence.workflowGoalText) {
12241
+ if (!teamSyncFilesOverlap(item.files, evidence.files)) {
12067
12242
  return false;
12068
12243
  }
12069
- if (!teamSyncFilesOverlap(item.files, evidence.files)) {
12244
+ return evidenceTexts.some((text) => significantTokenOverlap(itemText, text));
12245
+ }
12246
+ function matchesDirectCompletionEvidence(item, evidence) {
12247
+ const itemText = normalizeCompletionText(item.summary);
12248
+ if (!itemText) {
12070
12249
  return false;
12071
12250
  }
12072
- return Boolean(evidence.summaryText && significantTokenOverlap(itemText, evidence.summaryText));
12251
+ const evidenceTexts = [evidence.workflowGoalText, evidence.summaryText].filter(
12252
+ (value) => Boolean(value)
12253
+ );
12254
+ if (evidenceTexts.some((text) => completionTextsMatch(itemText, text))) {
12255
+ return true;
12256
+ }
12257
+ return false;
12073
12258
  }
12074
12259
  function normalizeCompletionText(value) {
12075
12260
  const normalized = value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
@@ -12429,16 +12614,16 @@ function upsertLocalAdaptiveRoutingPolicy(workerRole) {
12429
12614
  mode: stringValue3(existing.mode) ?? "catalog",
12430
12615
  plannerRetainsReasoning: existing.plannerRetainsReasoning !== false,
12431
12616
  preferLocalWorkers: true,
12432
- allowedEndpointTypes: uniqueStrings5([
12617
+ allowedEndpointTypes: uniqueStrings6([
12433
12618
  ...normalizeStringList2(existing.allowedEndpointTypes),
12434
12619
  "local",
12435
12620
  "cloud"
12436
12621
  ]),
12437
- preferredEndpointTypes: uniqueStrings5([
12622
+ preferredEndpointTypes: uniqueStrings6([
12438
12623
  "local",
12439
12624
  ...normalizeStringList2(existing.preferredEndpointTypes)
12440
12625
  ]),
12441
- allowedWorkerClasses: uniqueStrings5([
12626
+ allowedWorkerClasses: uniqueStrings6([
12442
12627
  ...normalizeStringList2(existing.allowedWorkerClasses),
12443
12628
  workerRole,
12444
12629
  "documentation",
@@ -12498,9 +12683,9 @@ function defaultCapabilitiesForWorker(workerRole) {
12498
12683
  }
12499
12684
  function normalizeStringList2(value) {
12500
12685
  const values = Array.isArray(value) ? value : value === void 0 ? [] : [value];
12501
- return uniqueStrings5(values.map(stringValue3).filter((item) => Boolean(item)));
12686
+ return uniqueStrings6(values.map(stringValue3).filter((item) => Boolean(item)));
12502
12687
  }
12503
- function uniqueStrings5(values) {
12688
+ function uniqueStrings6(values) {
12504
12689
  return Array.from(new Set(values));
12505
12690
  }
12506
12691
  function stringValue3(value) {
@@ -12645,7 +12830,7 @@ function numberValue2(value) {
12645
12830
  const parsed = Number(text);
12646
12831
  return Number.isFinite(parsed) ? parsed : void 0;
12647
12832
  }
12648
- function uniqueStrings6(values) {
12833
+ function uniqueStrings7(values) {
12649
12834
  return Array.from(new Set(values.filter((value) => value.trim().length > 0)));
12650
12835
  }
12651
12836
  function normalizeEnum(value) {
@@ -15255,8 +15440,8 @@ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
15255
15440
  runtimeCatalog: catalog,
15256
15441
  routingCard: {
15257
15442
  ...routing.routingCard,
15258
- reasons: uniqueStrings6(reasons),
15259
- warnings: uniqueStrings6([...routing.routingCard.warnings, ...gatewayWarnings])
15443
+ reasons: uniqueStrings7(reasons),
15444
+ warnings: uniqueStrings7([...routing.routingCard.warnings, ...gatewayWarnings])
15260
15445
  }
15261
15446
  };
15262
15447
  } catch (error) {
@@ -15273,7 +15458,7 @@ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
15273
15458
  },
15274
15459
  routingCard: {
15275
15460
  ...routing.routingCard,
15276
- warnings: uniqueStrings6([...routing.routingCard.warnings, warning])
15461
+ warnings: uniqueStrings7([...routing.routingCard.warnings, warning])
15277
15462
  }
15278
15463
  };
15279
15464
  }
@@ -15439,7 +15624,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
15439
15624
  const approvalRequired = booleanValue(
15440
15625
  isRecord6(resolution?.policyDecision) ? resolution.policyDecision.approvalRequired : void 0
15441
15626
  );
15442
- const gatewayWarnings = uniqueStrings6([
15627
+ const gatewayWarnings = uniqueStrings7([
15443
15628
  ...resolutionWarnings,
15444
15629
  ...selectedCandidate ? [] : ["Local orchestrator did not select a worker candidate and will fail closed."]
15445
15630
  ]);
@@ -15460,7 +15645,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
15460
15645
  ...routing.routingCard,
15461
15646
  ...selectedCandidate?.workerClass ? { recommendedWorkerClass: selectedCandidate.workerClass } : {},
15462
15647
  ...approvalRequired !== void 0 ? { humanApprovalRequired: approvalRequired } : {},
15463
- reasons: uniqueStrings6([
15648
+ reasons: uniqueStrings7([
15464
15649
  ...routing.routingCard.reasons,
15465
15650
  ...resolutionReasons,
15466
15651
  ...selectedCandidate ? [
@@ -15469,7 +15654,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
15469
15654
  ...stringValue4(selectedEndpoint?.model) ? [`selected local model ${String(selectedEndpoint?.model)}`] : []
15470
15655
  ] : ["local orchestrator could not resolve a concrete worker candidate"]
15471
15656
  ]),
15472
- warnings: uniqueStrings6([...routing.routingCard.warnings, ...gatewayWarnings])
15657
+ warnings: uniqueStrings7([...routing.routingCard.warnings, ...gatewayWarnings])
15473
15658
  }
15474
15659
  };
15475
15660
  } catch (error) {
@@ -15486,7 +15671,7 @@ function enrichAdaptiveRoutingWithLocalOrchestrator(routing, options, cwd = proc
15486
15671
  },
15487
15672
  routingCard: {
15488
15673
  ...routing.routingCard,
15489
- warnings: uniqueStrings6([...routing.routingCard.warnings, warning])
15674
+ warnings: uniqueStrings7([...routing.routingCard.warnings, warning])
15490
15675
  }
15491
15676
  };
15492
15677
  }
@@ -17289,8 +17474,8 @@ function buildWorkflowAdaptiveRouting(options, policy = null, intentWarnings = [
17289
17474
  ...routing,
17290
17475
  routingCard: {
17291
17476
  ...routing.routingCard,
17292
- reasons: uniqueStrings6([...routing.routingCard.reasons, ...policyReasons]),
17293
- warnings: uniqueStrings6([...routing.routingCard.warnings, ...policyWarnings])
17477
+ reasons: uniqueStrings7([...routing.routingCard.reasons, ...policyReasons]),
17478
+ warnings: uniqueStrings7([...routing.routingCard.warnings, ...policyWarnings])
17294
17479
  }
17295
17480
  };
17296
17481
  }
@@ -17306,7 +17491,7 @@ function normalizeAdaptiveRoutingMode(value) {
17306
17491
  return normalized === "off" || normalized === "recommend" || normalized === "catalog" ? normalized : null;
17307
17492
  }
17308
17493
  function normalizeAdaptiveWorkerClasses(values) {
17309
- return uniqueStrings6((values ?? []).map(canonicalAdaptiveWorkerClass)).filter(
17494
+ return uniqueStrings7((values ?? []).map(canonicalAdaptiveWorkerClass)).filter(
17310
17495
  (value) => ["documentation", "tests", "review", "coding"].includes(value)
17311
17496
  );
17312
17497
  }
@@ -21647,9 +21832,96 @@ function commandForPackageScript(info, scriptName, cwd) {
21647
21832
  }
21648
21833
  return `pnpm ${scriptName}`;
21649
21834
  }
21835
+ function shellQuote3(value) {
21836
+ return `'${value.replace(/'/g, "'\\''")}'`;
21837
+ }
21838
+ function commandInDirectory(root, cwd, command) {
21839
+ const relativeRoot = import_path4.default.relative(import_path4.default.resolve(cwd), import_path4.default.resolve(root));
21840
+ if (!relativeRoot) {
21841
+ return command;
21842
+ }
21843
+ return `cd ${shellQuote3(relativeRoot)} && ${command}`;
21844
+ }
21845
+ function isPythonSurface(filePath) {
21846
+ const basename7 = import_path4.default.basename(filePath);
21847
+ return filePath.endsWith(".py") || basename7 === "pyproject.toml" || basename7 === "requirements.txt" || basename7.startsWith("requirements-") || basename7 === "setup.py" || basename7 === "setup.cfg" || basename7 === "tox.ini";
21848
+ }
21849
+ function findPyproject(startPath, cwd) {
21850
+ const absoluteStart = import_path4.default.resolve(cwd, startPath);
21851
+ let current = import_fs4.default.existsSync(absoluteStart) && import_fs4.default.statSync(absoluteStart).isDirectory() ? absoluteStart : import_path4.default.dirname(absoluteStart);
21852
+ const stop = import_path4.default.parse(import_path4.default.resolve(cwd)).root;
21853
+ while (current !== stop) {
21854
+ const candidate = import_path4.default.join(current, "pyproject.toml");
21855
+ if (import_fs4.default.existsSync(candidate)) {
21856
+ return candidate;
21857
+ }
21858
+ const parent = import_path4.default.dirname(current);
21859
+ if (parent === current) {
21860
+ break;
21861
+ }
21862
+ current = parent;
21863
+ }
21864
+ return null;
21865
+ }
21866
+ function readPythonProject(pyprojectPath) {
21867
+ try {
21868
+ return {
21869
+ root: import_path4.default.dirname(pyprojectPath),
21870
+ pyprojectPath,
21871
+ text: import_fs4.default.readFileSync(pyprojectPath, "utf8")
21872
+ };
21873
+ } catch {
21874
+ return null;
21875
+ }
21876
+ }
21877
+ function inferPythonChecks(changedFiles, cwd) {
21878
+ const pyprojectFiles = unique4(
21879
+ changedFiles.filter(isPythonSurface).map((file) => findPyproject(file, cwd)).filter((file) => typeof file === "string")
21880
+ );
21881
+ const projects = pyprojectFiles.map(readPythonProject).filter((info) => Boolean(info));
21882
+ return projects.flatMap((info) => {
21883
+ const checks = [
21884
+ {
21885
+ kind: "test",
21886
+ title: "pytest for Python project",
21887
+ command: commandInDirectory(info.root, cwd, "python -m pytest"),
21888
+ source: "fallback",
21889
+ reason: `Detected Python project at ${import_path4.default.relative(cwd, info.pyprojectPath)}.`
21890
+ }
21891
+ ];
21892
+ if (/\[tool\.ruff(?:\.|\])/.test(info.text) || /\bruff\b/.test(info.text)) {
21893
+ checks.push({
21894
+ kind: "lint",
21895
+ title: "ruff check for Python project",
21896
+ command: commandInDirectory(info.root, cwd, "python -m ruff check ."),
21897
+ source: "fallback",
21898
+ reason: `Detected Ruff configuration or dependency in ${import_path4.default.relative(cwd, info.pyprojectPath)}.`
21899
+ });
21900
+ }
21901
+ if (/\[tool\.mypy\]/.test(info.text) || /\bmypy\b/.test(info.text)) {
21902
+ checks.push({
21903
+ kind: "type-check",
21904
+ title: "mypy for Python project",
21905
+ command: commandInDirectory(info.root, cwd, "python -m mypy ."),
21906
+ source: "fallback",
21907
+ reason: `Detected mypy configuration or dependency in ${import_path4.default.relative(cwd, info.pyprojectPath)}.`
21908
+ });
21909
+ }
21910
+ if (/\[build-system\]/.test(info.text)) {
21911
+ checks.push({
21912
+ kind: "build",
21913
+ title: "build Python package",
21914
+ command: commandInDirectory(info.root, cwd, "python -m build"),
21915
+ source: "fallback",
21916
+ reason: `Detected Python build system in ${import_path4.default.relative(cwd, info.pyprojectPath)}.`
21917
+ });
21918
+ }
21919
+ return checks;
21920
+ });
21921
+ }
21650
21922
  function inferPackageScriptChecks(changedFiles, cwd) {
21651
21923
  const packageFiles = unique4(
21652
- changedFiles.map((file) => findPackageJson(file, cwd)).filter((file) => typeof file === "string")
21924
+ changedFiles.filter((file) => !(isPythonSurface(file) && findPyproject(file, cwd))).map((file) => findPackageJson(file, cwd)).filter((file) => typeof file === "string")
21653
21925
  );
21654
21926
  const packages = packageFiles.map(readPackageInfo).filter((info) => Boolean(info));
21655
21927
  const scriptKinds = [
@@ -21777,8 +22049,14 @@ function buildVerificationPlan(options) {
21777
22049
  const impactedFiles = collectImpactedFiles(options.codeImpact, changedFiles, options.filePath);
21778
22050
  const directChecks = collectDirectTestChecks(options.codeImpact);
21779
22051
  const actionChecks = collectActionChecks(options.codeImpact);
22052
+ const pythonChecks = inferPythonChecks(impactedFiles, cwd);
21780
22053
  const packageChecks = inferPackageScriptChecks(impactedFiles, cwd);
21781
- const recommendedChecks = dedupeChecks([...directChecks, ...actionChecks, ...packageChecks]);
22054
+ const recommendedChecks = dedupeChecks([
22055
+ ...directChecks,
22056
+ ...actionChecks,
22057
+ ...pythonChecks,
22058
+ ...packageChecks
22059
+ ]);
21782
22060
  const missingChecks = collectCoverageGaps(options.codeImpact);
21783
22061
  if (!options.codeImpact) {
21784
22062
  missingChecks.push({
@@ -232,7 +232,10 @@ longer timeout, retries once with a shorter summary on transient hosted failures
232
232
  and then records a local fallback handoff in `.snipara/team-sync/session.json`
233
233
  if the hosted call still times out. A hosted final-commit timeout does not modify
234
234
  Git state. Custom final-commit categories are namespaced under `final-commit`
235
- before the hosted call so they stay on the handoff-only path.
235
+ before the hosted call so they stay on the handoff-only path. Completed workflow
236
+ commits also reconcile local Team Sync work: exact goal/summary matches close
237
+ directly, and slug-like workflow goals can still close the matching active work
238
+ when touched files overlap and meaningful workflow tokens match.
236
239
 
237
240
  ## Adaptive Work Routing
238
241
 
@@ -961,6 +964,7 @@ Semantics:
961
964
  - `snipara-companion workflow phase-start` = marks the current phase and prints the required Snipara context gate plus code-impact / symbol-card gates; runtime-marked phases also get a stable Snipara Sandbox session binding
962
965
  - `snipara-companion workflow runtime-checkpoint` = captures a resume-ready Snipara Sandbox checkpoint for one phase using local workflow state plus a hosted automation event when configured
963
966
  - `snipara-companion workflow phase-commit` = calls hosted `snipara_end_of_task_commit` for that phase, updates local state, and advances the next phase; if the hosted commit times out or hits a transient network failure, local workflow state still advances with an explicit local fallback record
967
+ - `snipara-companion workflow phase-commit` and `workflow final-commit` also complete matching local Team Sync active work when the workflow is completed. Matching is conservative: exact workflow goal/summary text wins, and file overlap plus meaningful token overlap handles slug-like workflow goals without closing unrelated active work.
964
968
  - `snipara-companion workflow impact-gate` = local pre-push gate for completed workflow phases in `upstream..HEAD`; it keeps dirty files out of the committed impact analysis and reports phase/file coverage before hosted reindex catches up
965
969
  - `snipara-companion workflow resume` = reloads local workflow state plus hosted durable memory after compaction or resume, optionally includes short-lived session context with `--include-session-context`, then appends the latest hosted Team Sync handoff/checkpoint context when available; runtime-bound phases also print a Snipara Sandbox reattach or rehydrate plan; rerun `workflow phase-start` before editing again
966
970
  - `snipara-companion workflow resume` does not snapshot or exactly restore a live Snipara Sandbox process; exact process restore remains a roadmap item
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Local-first CLI that asks your repo what breaks before an AI coding agent edits it.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {