snipara-companion 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -112,6 +112,14 @@ The mental model is intentionally close to Git:
112
112
  | `git format-patch` | `snipara-companion handoff` |
113
113
  | `git checkout` | `snipara-companion workflow resume` |
114
114
 
115
+ `snipara-companion final-commit` closes the local workflow and asks the hosted
116
+ API only for the final Team Sync handoff. The CLI sends a compact summary with a
117
+ longer timeout, retries once with a shorter summary on transient hosted failures,
118
+ and then records a local fallback handoff in `.snipara/team-sync/session.json`
119
+ if the hosted call still times out. A hosted final-commit timeout does not modify
120
+ Git state. Custom final-commit categories are namespaced under `final-commit`
121
+ before the hosted call so they stay on the handoff-only path.
122
+
115
123
  ## Verification Plans
116
124
 
117
125
  Use `verify` when an agent asks what to prove before handoff or release:
@@ -489,6 +497,7 @@ project API key, the same commands also call the hosted Team Sync surfaces:
489
497
  - `team-sync handoff` records the local handoff and publishes the hosted handoff capsule.
490
498
  - `team-sync what-changed` keeps local counters but also loads the hosted What Changed For Me surface.
491
499
  - `team-sync resume` and `workflow resume` append the latest hosted handoff plus checkpoint-aware resume context when available.
500
+ - `team-sync sweep` archives local work items after 14 days without update by default; use `--dry-run` to review before changing the local continuity file.
492
501
  - Hosted MCP also exposes `snipara_resume_context` for agents that want the same continuity bundle directly: latest handoff match, What Changed, active decisions, execution-memory, and an optional task-scoped work brief.
493
502
 
494
503
  Typical flow:
@@ -497,6 +506,7 @@ Typical flow:
497
506
  snipara-companion team-sync start-work --summary "add invite permissions" --files apps/web/src/lib/auth/permissions.ts
498
507
  snipara-companion team-sync handoff --summary "moved project access check" --next "run permissions tests before merge" --attention proof
499
508
  snipara-companion team-sync what-changed
509
+ snipara-companion team-sync sweep --dry-run
500
510
  snipara-companion workflow resume --include-session-context
501
511
  snipara-companion workflow phase-start implement-permissions
502
512
  ```
@@ -584,6 +594,7 @@ Semantics:
584
594
  - `snipara-companion team-sync start-work` = keeps the local session file and fetches the hosted Start Work Brief when the workspace has project auth
585
595
  - `snipara-companion team-sync handoff` = keeps the local handoff record and publishes the hosted handoff capsule when project auth is available
586
596
  - `snipara-companion team-sync what-changed` = prints the local state summary and the hosted What Changed For Me response when configured
597
+ - `snipara-companion team-sync sweep` = archives stale local work items after an inactivity threshold; default is 14 days and `--dry-run` previews the cleanup
587
598
  - `snipara-companion team-sync resume` = reloads local carryover plus the hosted latest handoff and checkpoint-aware resume guidance when available
588
599
  - `snipara-companion final-commit` / `workflow final-commit` = final hosted commit for the managed workflow
589
600
  - `snipara-companion code symbol-card` = direct paid Context `snipara_code_symbol_card` for an important symbol before editing, with an agent guidance summary before raw JSON
package/dist/index.d.ts CHANGED
@@ -949,6 +949,8 @@ declare class RLMClient {
949
949
  category?: string;
950
950
  outcome?: "completed" | "partial" | "blocked" | "abandoned";
951
951
  filesTouched?: string[];
952
+ persistTypes?: string[];
953
+ handoffOnly?: boolean;
952
954
  }): Promise<Record<string, unknown>>;
953
955
  journalAppend(text: string, tags?: string[]): Promise<JournalAppendResult>;
954
956
  }
@@ -1209,6 +1211,7 @@ interface AgenticWorkStatus {
1209
1211
  teamSync: {
1210
1212
  activeWorkCount: number;
1211
1213
  staleWorkCount: number;
1214
+ archivedWorkCount: number;
1212
1215
  handoffCount: number;
1213
1216
  latestHandoff?: {
1214
1217
  summary: string;
@@ -1501,7 +1504,7 @@ declare function getAutomationStatus(projectDir?: string): AutomationStatusResul
1501
1504
 
1502
1505
  declare const TEAM_SYNC_STATE_RELATIVE_PATH: string;
1503
1506
  type TeamSyncAttention = "note" | "watch" | "review" | "proof";
1504
- type TeamSyncWorkStatus = "active" | "completed";
1507
+ type TeamSyncWorkStatus = "active" | "completed" | "archived";
1505
1508
  interface TeamSyncRecordInput {
1506
1509
  summary: string;
1507
1510
  files?: string[];
@@ -1524,6 +1527,8 @@ interface TeamSyncWorkRecord {
1524
1527
  updatedAt: string;
1525
1528
  completedAt?: string;
1526
1529
  completionReason?: string;
1530
+ archivedAt?: string;
1531
+ archiveReason?: string;
1527
1532
  }
1528
1533
  interface TeamSyncHandoffRecord {
1529
1534
  id: string;
@@ -1545,18 +1550,50 @@ interface TeamSyncSummary {
1545
1550
  activeWorkCount: number;
1546
1551
  staleWorkCount: number;
1547
1552
  completedWorkCount: number;
1553
+ archivedWorkCount: number;
1548
1554
  handoffCount: number;
1549
1555
  files: string[];
1550
1556
  latestActiveWork?: TeamSyncWorkRecord;
1551
1557
  latestStaleWork?: TeamSyncWorkRecord;
1552
1558
  latestCompletedWork?: TeamSyncWorkRecord;
1559
+ latestArchivedWork?: TeamSyncWorkRecord;
1553
1560
  latestHandoff?: TeamSyncHandoffRecord;
1554
1561
  }
1562
+ interface TeamSyncCommandOptions {
1563
+ id?: string;
1564
+ summary?: string;
1565
+ files?: string[];
1566
+ branch?: string;
1567
+ actor?: string;
1568
+ next?: string;
1569
+ attention?: string;
1570
+ risk?: string;
1571
+ since?: string;
1572
+ dir?: string;
1573
+ includeSessionContext?: boolean;
1574
+ emitOrchestratorHandoff?: boolean;
1575
+ autoRouteOrchestrator?: boolean;
1576
+ orchestratorPolicySource?: string;
1577
+ output?: string;
1578
+ days?: string;
1579
+ dryRun?: boolean;
1580
+ json?: boolean;
1581
+ }
1555
1582
  interface HostedAttempt<T> {
1556
1583
  status: "skipped" | "ok" | "error";
1557
1584
  data?: T;
1558
1585
  error?: string;
1559
1586
  }
1587
+ interface TeamSyncSweepResult {
1588
+ action: "sweep";
1589
+ statePath: string;
1590
+ dryRun: boolean;
1591
+ thresholdDays: number;
1592
+ thresholdMs: number;
1593
+ archivedCount: number;
1594
+ archivedWork: TeamSyncWorkRecord[];
1595
+ summary: TeamSyncSummary;
1596
+ }
1560
1597
  interface AgenticHandoffArtifact {
1561
1598
  version: "snipara.agentic_handoff.v1";
1562
1599
  generatedAt: string;
@@ -1581,10 +1618,17 @@ declare function buildTeamSyncStartWorkRecord(input: TeamSyncRecordInput): TeamS
1581
1618
  declare function buildTeamSyncHandoffRecord(input: TeamSyncRecordInput): TeamSyncHandoffRecord;
1582
1619
  declare function createEmptyTeamSyncState(now?: Date): TeamSyncState;
1583
1620
  declare function buildTeamSyncSummary(state: TeamSyncState, since?: Date, now?: Date): TeamSyncSummary;
1621
+ declare function archiveInactiveTeamSyncWork(state: TeamSyncState, options?: {
1622
+ now?: Date;
1623
+ thresholdMs?: number;
1624
+ dryRun?: boolean;
1625
+ }): TeamSyncSweepResult["archivedWork"];
1626
+ declare function autoArchiveTeamSyncState(rootDir?: string, now?: Date): TeamSyncSweepResult["archivedWork"];
1584
1627
  declare function loadTeamSyncState(rootDir?: string): TeamSyncState;
1585
1628
  declare function saveTeamSyncState(state: TeamSyncState, rootDir?: string): void;
1586
1629
  declare function getTeamSyncStatePath(rootDir?: string): string;
1587
1630
  declare function buildAgenticHandoffMarkdown(artifact: AgenticHandoffArtifact): string;
1631
+ declare function teamSyncSweepCommand(options: TeamSyncCommandOptions): Promise<void>;
1588
1632
 
1589
1633
  interface JournalCheckpointPayload {
1590
1634
  action: string;
@@ -1773,4 +1817,4 @@ interface WrittenOrchestratorHandoff {
1773
1817
  declare function buildOrchestratorHandoff(options: OrchestratorHandoffOptions): OrchestratorHandoffArtifact;
1774
1818
  declare function writeOrchestratorHandoff(options: OrchestratorHandoffOptions): WrittenOrchestratorHandoff;
1775
1819
 
1776
- export { AUTOMATION_MANIFEST_RELATIVE_PATH, AutomationInstallConflictError, AutomationUnsupportedHookBundleError, ORCHESTRATOR_HANDOFF_RELATIVE_PATH, TEAM_SYNC_STATE_RELATIVE_PATH, WORKFLOW_PLANS_RELATIVE_DIR, WORKFLOW_STATE_RELATIVE_PATH, buildAgenticHandoffMarkdown, buildAgenticTimeline, buildAgenticWorkStatus, buildAutomationInstallPlan, buildCanonicalEvent, buildJournalCheckpointEntry, buildOnboardFolderManifest, buildOrchestratorHandoff, buildProjectIntelligenceBrief, buildSyncDocumentsDryRun, buildTeamSyncHandoffRecord, buildTeamSyncStartWorkRecord, buildTeamSyncSummary, buildToolCallPayload, buildToolResultPayload, buildVerificationPlan, buildWorkflowPhaseCommitSummary, buildWorkflowPlanScaffold, categoryFromGuardTag, classifyToolResult, collectSyncDocuments, collectSyncDocumentsInput, createClient, createEmptyTeamSyncState, createLocalQueryCache, detectReleaseSurfacesFromFiles, detectRuntimeEnvironment, extractCommandFromToolInput, extractFilesFromToolInput, formatOrchestratorRecommendationReason, formatStuckGuardDecision, getAutomationManifestPath, getAutomationStatus, getConfigPath, getOrchestratorRecommendation, getPlanStepDisplayTitle, getStagedFiles, getStuckGuardInjection, getTeamSyncStatePath, installAutomationBundle, listProjectsForApiKey, loadAutomationManifest, loadConfig, loadTeamSyncState, normalizeGuardTag, normalizeWorkflowPlanInput, projectIntelligenceBriefCommand, resolveQueryFromToolInput, runMemoryGuardCheck, saveConfig, saveTeamSyncState, shouldSuggestOrchestratorForWorkflow, shouldSuggestRuntimeForWorkflow, verifyCommand, writeOrchestratorHandoff };
1820
+ export { AUTOMATION_MANIFEST_RELATIVE_PATH, AutomationInstallConflictError, AutomationUnsupportedHookBundleError, ORCHESTRATOR_HANDOFF_RELATIVE_PATH, TEAM_SYNC_STATE_RELATIVE_PATH, WORKFLOW_PLANS_RELATIVE_DIR, WORKFLOW_STATE_RELATIVE_PATH, archiveInactiveTeamSyncWork, autoArchiveTeamSyncState, buildAgenticHandoffMarkdown, buildAgenticTimeline, buildAgenticWorkStatus, buildAutomationInstallPlan, buildCanonicalEvent, buildJournalCheckpointEntry, buildOnboardFolderManifest, buildOrchestratorHandoff, buildProjectIntelligenceBrief, buildSyncDocumentsDryRun, buildTeamSyncHandoffRecord, buildTeamSyncStartWorkRecord, buildTeamSyncSummary, buildToolCallPayload, buildToolResultPayload, buildVerificationPlan, buildWorkflowPhaseCommitSummary, buildWorkflowPlanScaffold, categoryFromGuardTag, classifyToolResult, collectSyncDocuments, collectSyncDocumentsInput, createClient, createEmptyTeamSyncState, createLocalQueryCache, detectReleaseSurfacesFromFiles, detectRuntimeEnvironment, extractCommandFromToolInput, extractFilesFromToolInput, formatOrchestratorRecommendationReason, formatStuckGuardDecision, getAutomationManifestPath, getAutomationStatus, getConfigPath, getOrchestratorRecommendation, getPlanStepDisplayTitle, getStagedFiles, getStuckGuardInjection, getTeamSyncStatePath, installAutomationBundle, listProjectsForApiKey, loadAutomationManifest, loadConfig, loadTeamSyncState, normalizeGuardTag, normalizeWorkflowPlanInput, projectIntelligenceBriefCommand, resolveQueryFromToolInput, runMemoryGuardCheck, saveConfig, saveTeamSyncState, shouldSuggestOrchestratorForWorkflow, shouldSuggestRuntimeForWorkflow, teamSyncSweepCommand, verifyCommand, writeOrchestratorHandoff };
package/dist/index.js CHANGED
@@ -38,6 +38,8 @@ __export(index_exports, {
38
38
  TEAM_SYNC_STATE_RELATIVE_PATH: () => TEAM_SYNC_STATE_RELATIVE_PATH,
39
39
  WORKFLOW_PLANS_RELATIVE_DIR: () => WORKFLOW_PLANS_RELATIVE_DIR,
40
40
  WORKFLOW_STATE_RELATIVE_PATH: () => WORKFLOW_STATE_RELATIVE_PATH,
41
+ archiveInactiveTeamSyncWork: () => archiveInactiveTeamSyncWork,
42
+ autoArchiveTeamSyncState: () => autoArchiveTeamSyncState,
41
43
  buildAgenticHandoffMarkdown: () => buildAgenticHandoffMarkdown,
42
44
  buildAgenticTimeline: () => buildAgenticTimeline,
43
45
  buildAgenticWorkStatus: () => buildAgenticWorkStatus,
@@ -91,6 +93,7 @@ __export(index_exports, {
91
93
  saveTeamSyncState: () => saveTeamSyncState,
92
94
  shouldSuggestOrchestratorForWorkflow: () => shouldSuggestOrchestratorForWorkflow,
93
95
  shouldSuggestRuntimeForWorkflow: () => shouldSuggestRuntimeForWorkflow,
96
+ teamSyncSweepCommand: () => teamSyncSweepCommand,
94
97
  verifyCommand: () => verifyCommand,
95
98
  writeOrchestratorHandoff: () => writeOrchestratorHandoff
96
99
  });
@@ -1301,7 +1304,8 @@ var RLMClient = class {
1301
1304
  category: args.category,
1302
1305
  outcome: args.outcome || "completed",
1303
1306
  files_touched: args.filesTouched || [],
1304
- persist_types: ["decision", "learning", "workflow"]
1307
+ persist_types: args.persistTypes ?? ["decision", "learning", "workflow"],
1308
+ ...args.handoffOnly !== void 0 ? { handoff_only: args.handoffOnly } : {}
1305
1309
  });
1306
1310
  }
1307
1311
  async journalAppend(text, tags) {
@@ -7049,6 +7053,7 @@ async function appendJournalCheckpoint(payload) {
7049
7053
  // src/commands/team-sync.ts
7050
7054
  var TEAM_SYNC_STATE_RELATIVE_PATH = path12.join(".snipara", "team-sync", "session.json");
7051
7055
  var TEAM_SYNC_STALE_WORK_MS = 48 * 60 * 60 * 1e3;
7056
+ var TEAM_SYNC_AUTO_ARCHIVE_WORK_MS = 14 * 24 * 60 * 60 * 1e3;
7052
7057
  function buildTeamSyncStartWorkRecord(input) {
7053
7058
  const createdAt = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
7054
7059
  const files = normalizeFiles(input.files);
@@ -7094,6 +7099,7 @@ function buildTeamSyncSummary(state, since, now = /* @__PURE__ */ new Date()) {
7094
7099
  const activeWork = work.filter((item) => item.status === "active" && !isStaleWork(item, now));
7095
7100
  const staleWork = work.filter((item) => item.status === "active" && isStaleWork(item, now));
7096
7101
  const completedWork = work.filter((item) => item.status === "completed");
7102
+ const archivedWork = work.filter((item) => item.status === "archived");
7097
7103
  const files = /* @__PURE__ */ new Set();
7098
7104
  for (const item of [...work, ...handoffs]) {
7099
7105
  for (const file of item.files) {
@@ -7104,14 +7110,45 @@ function buildTeamSyncSummary(state, since, now = /* @__PURE__ */ new Date()) {
7104
7110
  activeWorkCount: activeWork.length,
7105
7111
  staleWorkCount: staleWork.length,
7106
7112
  completedWorkCount: completedWork.length,
7113
+ archivedWorkCount: archivedWork.length,
7107
7114
  handoffCount: handoffs.length,
7108
7115
  files: Array.from(files).sort(),
7109
7116
  latestActiveWork: activeWork[activeWork.length - 1],
7110
7117
  latestStaleWork: staleWork[staleWork.length - 1],
7111
7118
  latestCompletedWork: completedWork[completedWork.length - 1],
7119
+ latestArchivedWork: archivedWork[archivedWork.length - 1],
7112
7120
  latestHandoff: handoffs[handoffs.length - 1]
7113
7121
  };
7114
7122
  }
7123
+ function archiveInactiveTeamSyncWork(state, options = {}) {
7124
+ const now = options.now ?? /* @__PURE__ */ new Date();
7125
+ const nowIso2 = now.toISOString();
7126
+ const thresholdMs = options.thresholdMs ?? TEAM_SYNC_AUTO_ARCHIVE_WORK_MS;
7127
+ const thresholdDays = Math.round(thresholdMs / (24 * 60 * 60 * 1e3));
7128
+ const candidates = state.work.filter(
7129
+ (item) => item.status === "active" && now.getTime() - getWorkTimestamp(item) > thresholdMs
7130
+ );
7131
+ if (!options.dryRun) {
7132
+ for (const item of candidates) {
7133
+ item.status = "archived";
7134
+ item.updatedAt = nowIso2;
7135
+ item.archivedAt = nowIso2;
7136
+ item.archiveReason = `No update for ${thresholdDays} day(s)`;
7137
+ }
7138
+ if (candidates.length > 0) {
7139
+ state.updatedAt = nowIso2;
7140
+ }
7141
+ }
7142
+ return candidates;
7143
+ }
7144
+ function autoArchiveTeamSyncState(rootDir = process.cwd(), now = /* @__PURE__ */ new Date()) {
7145
+ const state = loadTeamSyncState(rootDir);
7146
+ const archivedWork = archiveInactiveTeamSyncWork(state, { now });
7147
+ if (archivedWork.length > 0) {
7148
+ saveTeamSyncState(state, rootDir);
7149
+ }
7150
+ return archivedWork;
7151
+ }
7115
7152
  function loadTeamSyncState(rootDir = process.cwd()) {
7116
7153
  const statePath = getTeamSyncStatePath(rootDir);
7117
7154
  if (!fs13.existsSync(statePath)) {
@@ -7137,6 +7174,7 @@ function getTeamSyncStatePath(rootDir = process.cwd()) {
7137
7174
  async function teamSyncStartWorkCommand(options) {
7138
7175
  const rootDir = resolveRootDir(options.dir);
7139
7176
  const state = loadTeamSyncState(rootDir);
7177
+ archiveInactiveTeamSyncWork(state);
7140
7178
  const branch = normalizeOptionalString(options.branch) ?? readGitValue2(rootDir, ["branch", "--show-current"]);
7141
7179
  const record = buildTeamSyncStartWorkRecord({
7142
7180
  summary: options.summary ?? "",
@@ -7202,6 +7240,7 @@ async function teamSyncStartWorkCommand(options) {
7202
7240
  }
7203
7241
  async function teamSyncHandoffCommand(options) {
7204
7242
  const rootDir = resolveRootDir(options.dir);
7243
+ autoArchiveTeamSyncState(rootDir);
7205
7244
  const payload = await createTeamSyncHandoffPayload(rootDir, {
7206
7245
  ...options,
7207
7246
  summary: options.summary ?? ""
@@ -7210,6 +7249,7 @@ async function teamSyncHandoffCommand(options) {
7210
7249
  }
7211
7250
  async function createTeamSyncHandoffPayload(rootDir, options) {
7212
7251
  const state = loadTeamSyncState(rootDir);
7252
+ archiveInactiveTeamSyncWork(state);
7213
7253
  const record = buildTeamSyncHandoffRecord({
7214
7254
  summary: options.summary,
7215
7255
  files: options.files,
@@ -7459,6 +7499,10 @@ async function teamSyncWhatChangedCommand(options) {
7459
7499
  const rootDir = resolveRootDir(options.dir);
7460
7500
  const since = parseSince(options.since);
7461
7501
  const state = loadTeamSyncState(rootDir);
7502
+ const archivedWork = archiveInactiveTeamSyncWork(state);
7503
+ if (archivedWork.length > 0) {
7504
+ saveTeamSyncState(state, rootDir);
7505
+ }
7462
7506
  const summary = buildTeamSyncSummary(state, since);
7463
7507
  const hosted = await maybeGetHostedWhatChanged(rootDir, {
7464
7508
  since: since?.toISOString(),
@@ -7506,10 +7550,14 @@ async function teamSyncWhatChangedCommand(options) {
7506
7550
  async function teamSyncCompleteWorkCommand(options) {
7507
7551
  const rootDir = resolveRootDir(options.dir);
7508
7552
  const state = loadTeamSyncState(rootDir);
7553
+ const archivedWork = archiveInactiveTeamSyncWork(state);
7509
7554
  const now = (/* @__PURE__ */ new Date()).toISOString();
7510
7555
  const activeOrStale = state.work.filter((item) => item.status === "active");
7511
7556
  const record = options.id ? activeOrStale.find((item) => item.id === options.id) : activeOrStale[activeOrStale.length - 1];
7512
7557
  if (!record) {
7558
+ if (archivedWork.length > 0) {
7559
+ saveTeamSyncState(state, rootDir);
7560
+ }
7513
7561
  throw new Error(
7514
7562
  options.id ? `No active Team Sync work item found for id ${options.id}` : "No active Team Sync work item to complete"
7515
7563
  );
@@ -7545,6 +7593,10 @@ async function teamSyncCompleteWorkCommand(options) {
7545
7593
  async function teamSyncResumeCommand(options) {
7546
7594
  const rootDir = resolveRootDir(options.dir);
7547
7595
  const state = loadTeamSyncState(rootDir);
7596
+ const archivedWork = archiveInactiveTeamSyncWork(state);
7597
+ if (archivedWork.length > 0) {
7598
+ saveTeamSyncState(state, rootDir);
7599
+ }
7548
7600
  const summary = buildTeamSyncSummary(state);
7549
7601
  const hosted = await maybeGetHostedResume(rootDir, {
7550
7602
  task: summary.latestActiveWork?.summary ?? summary.latestHandoff?.summary,
@@ -7592,6 +7644,31 @@ async function teamSyncResumeCommand(options) {
7592
7644
  options.json
7593
7645
  );
7594
7646
  }
7647
+ async function teamSyncSweepCommand(options) {
7648
+ const rootDir = resolveRootDir(options.dir);
7649
+ const thresholdDays = parsePositiveDays(options.days ?? "14");
7650
+ const thresholdMs = thresholdDays * 24 * 60 * 60 * 1e3;
7651
+ const state = loadTeamSyncState(rootDir);
7652
+ const archivedWork = archiveInactiveTeamSyncWork(state, {
7653
+ thresholdMs,
7654
+ dryRun: Boolean(options.dryRun)
7655
+ });
7656
+ if (!options.dryRun && archivedWork.length > 0) {
7657
+ saveTeamSyncState(state, rootDir);
7658
+ }
7659
+ const summary = buildTeamSyncSummary(state);
7660
+ const payload = {
7661
+ action: "sweep",
7662
+ statePath: getTeamSyncStatePath(rootDir),
7663
+ dryRun: Boolean(options.dryRun),
7664
+ thresholdDays,
7665
+ thresholdMs,
7666
+ archivedCount: archivedWork.length,
7667
+ archivedWork,
7668
+ summary
7669
+ };
7670
+ printTeamSyncSweepResult(payload, options.json);
7671
+ }
7595
7672
  function printTeamSyncResult(payload, json) {
7596
7673
  if (json) {
7597
7674
  console.log(JSON.stringify(payload, null, 2));
@@ -7604,6 +7681,7 @@ function printTeamSyncResult(payload, json) {
7604
7681
  console.log(`Active work: ${summary.activeWorkCount}`);
7605
7682
  console.log(`Stale work: ${summary.staleWorkCount}`);
7606
7683
  console.log(`Completed work: ${summary.completedWorkCount}`);
7684
+ console.log(`Archived work: ${summary.archivedWorkCount}`);
7607
7685
  console.log(`Handoffs: ${summary.handoffCount}`);
7608
7686
  if (summary.files.length > 0) {
7609
7687
  console.log(`Files: ${summary.files.slice(0, 8).join(", ")}`);
@@ -7617,6 +7695,9 @@ function printTeamSyncResult(payload, json) {
7617
7695
  if (summary.latestCompletedWork) {
7618
7696
  console.log(`Latest completed work: ${summary.latestCompletedWork.summary}`);
7619
7697
  }
7698
+ if (summary.latestArchivedWork) {
7699
+ console.log(`Latest archived work: ${summary.latestArchivedWork.summary}`);
7700
+ }
7620
7701
  if (summary.latestHandoff) {
7621
7702
  console.log(`Latest handoff: ${summary.latestHandoff.summary}`);
7622
7703
  if (summary.latestHandoff.next) {
@@ -7666,6 +7747,25 @@ function printTeamSyncResult(payload, json) {
7666
7747
  }
7667
7748
  }
7668
7749
  }
7750
+ function printTeamSyncSweepResult(payload, json) {
7751
+ if (json) {
7752
+ console.log(JSON.stringify(payload, null, 2));
7753
+ return;
7754
+ }
7755
+ console.log(payload.dryRun ? "Team Sync sweep preview" : "Team Sync sweep completed");
7756
+ console.log(`State: ${payload.statePath}`);
7757
+ console.log(`Threshold: ${payload.thresholdDays} day(s) without update`);
7758
+ console.log(`Archived work: ${payload.archivedCount}`);
7759
+ if (payload.archivedWork.length > 0) {
7760
+ console.log(
7761
+ `Items: ${formatList(
7762
+ payload.archivedWork.map((item) => item.summary),
7763
+ 6
7764
+ )}`
7765
+ );
7766
+ }
7767
+ console.log(`Remaining stale work: ${payload.summary.staleWorkCount}`);
7768
+ }
7669
7769
  function printHostedWorkBrief(brief) {
7670
7770
  console.log("");
7671
7771
  console.log("Hosted Start Work Brief");
@@ -8054,13 +8154,22 @@ function normalizeWorkRecord(record) {
8054
8154
  files: normalizeFiles(parsed.files),
8055
8155
  branch: normalizeOptionalString(parsed.branch),
8056
8156
  actor: normalizeOptionalString(parsed.actor),
8057
- status: parsed.status === "completed" ? "completed" : "active",
8157
+ status: parsed.status === "completed" || parsed.status === "archived" ? parsed.status : "active",
8058
8158
  createdAt,
8059
8159
  updatedAt,
8060
8160
  completedAt: normalizeOptionalString(parsed.completedAt),
8061
- completionReason: normalizeOptionalString(parsed.completionReason)
8161
+ completionReason: normalizeOptionalString(parsed.completionReason),
8162
+ archivedAt: normalizeOptionalString(parsed.archivedAt),
8163
+ archiveReason: normalizeOptionalString(parsed.archiveReason)
8062
8164
  };
8063
8165
  }
8166
+ function parsePositiveDays(value) {
8167
+ const days = Number.parseInt(value, 10);
8168
+ if (!Number.isFinite(days) || days <= 0) {
8169
+ throw new Error("--days must be a positive integer");
8170
+ }
8171
+ return days;
8172
+ }
8064
8173
  function parseSince(value) {
8065
8174
  if (!value) return void 0;
8066
8175
  const date = new Date(value);
@@ -8126,6 +8235,11 @@ var import_chalk5 = __toESM(require("chalk"));
8126
8235
  var DEFAULT_SESSION_CONTEXT_TOKENS = 1e3;
8127
8236
  var DEFAULT_FULL_WORKFLOW_CRITICAL_TOKENS = 2e3;
8128
8237
  var DEFAULT_SHARED_CONTEXT_TOKENS = 2e3;
8238
+ var TASK_COMMIT_TIMEOUT_MS = 3e4;
8239
+ var FINAL_COMMIT_TIMEOUT_MS = 9e4;
8240
+ var FINAL_COMMIT_RETRY_TIMEOUT_MS = 45e3;
8241
+ var FINAL_COMMIT_SUMMARY_MAX_CHARS = 1200;
8242
+ var FINAL_COMMIT_RETRY_SUMMARY_MAX_CHARS = 600;
8129
8243
  var SHARED_CONTEXT_INTENT_PATTERN = /\b(standard|standards|convention|conventions|guideline|guidelines|best practice|best practices|policy|policies|compliance|compliant|security rules|team rules|style guide|playbook|checklist)\b/i;
8130
8244
  var DOCUMENT_SYNC_FORMATS = {
8131
8245
  ".adoc": { kind: "DOC", format: "adoc" },
@@ -8361,6 +8475,52 @@ function toPreview(value, maxLength = 160) {
8361
8475
  }
8362
8476
  return "n/a";
8363
8477
  }
8478
+ function positiveIntegerEnv(name, fallback) {
8479
+ const value = process.env[name];
8480
+ if (!value) {
8481
+ return fallback;
8482
+ }
8483
+ const parsed = Number.parseInt(value, 10);
8484
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
8485
+ }
8486
+ function compactWhitespace(value) {
8487
+ return value.replace(/\s+/g, " ").trim();
8488
+ }
8489
+ function truncateText(value, maxLength) {
8490
+ const normalized = compactWhitespace(value);
8491
+ if (normalized.length <= maxLength) {
8492
+ return normalized;
8493
+ }
8494
+ const suffix = " ... [truncated locally for hosted final-commit]";
8495
+ return `${normalized.slice(0, Math.max(0, maxLength - suffix.length)).trimEnd()}${suffix}`;
8496
+ }
8497
+ function buildHostedFinalCommitSummary(args) {
8498
+ const prefix = args.workflowId ? `Workflow ${args.workflowId}
8499
+ Final commit
8500
+ ` : "";
8501
+ const budget = Math.max(80, args.maxLength - prefix.length);
8502
+ return `${prefix}${truncateText(args.summary, budget)}`;
8503
+ }
8504
+ function hostedCommitErrorMessage(error) {
8505
+ return error instanceof Error ? error.message : String(error);
8506
+ }
8507
+ function shouldRetryHostedFinalCommit(error) {
8508
+ const message = hostedCommitErrorMessage(error).toLowerCase();
8509
+ if (error instanceof Error && error.name === "AbortError") {
8510
+ return true;
8511
+ }
8512
+ return /abort|timeout|timed out|network|fetch|econn|etimedout|http 5\d\d/.test(message);
8513
+ }
8514
+ function isFinalCommitCategory(category) {
8515
+ return Boolean(category?.toLowerCase().includes("final-commit"));
8516
+ }
8517
+ function normalizeFinalCommitCategory(category) {
8518
+ const normalized = compactWhitespace(category ?? "");
8519
+ if (!normalized) {
8520
+ return "final-commit";
8521
+ }
8522
+ return isFinalCommitCategory(normalized) ? normalized : `final-commit:${normalized}`;
8523
+ }
8364
8524
  function getPlanStepDisplayTitle(step, index = 0) {
8365
8525
  if (typeof step === "string") {
8366
8526
  return toPreview(step);
@@ -10068,6 +10228,12 @@ function printSessionBootstrap(result, options) {
10068
10228
  function printTaskCommitResult(result) {
10069
10229
  printKeyValue("Tool:", "snipara_end_of_task_commit");
10070
10230
  printCompactObject(result, ["stored", "skipped", "status", "message"]);
10231
+ const handoff = isRecord6(result.team_sync_handoff) ? result.team_sync_handoff : null;
10232
+ if (handoff) {
10233
+ const status = typeof handoff.status === "string" ? handoff.status : "unknown";
10234
+ const memoryId = typeof handoff.memory_id === "string" ? ` (${handoff.memory_id})` : "";
10235
+ printKeyValue("Team Sync handoff:", `${status}${memoryId}`);
10236
+ }
10071
10237
  console.log("");
10072
10238
  printJson(result);
10073
10239
  }
@@ -11559,6 +11725,7 @@ function buildSuggestedAgenticNextAction(state, risks) {
11559
11725
  function buildAgenticWorkStatus(cwd = process.cwd()) {
11560
11726
  const state = readWorkflowState2(cwd);
11561
11727
  const git = readLocalGitState(cwd);
11728
+ autoArchiveTeamSyncState(cwd);
11562
11729
  const teamSyncState = loadTeamSyncState(cwd);
11563
11730
  const teamSyncSummary = buildTeamSyncSummary(teamSyncState);
11564
11731
  const latestHandoff = latestTeamSyncHandoff(teamSyncState.handoffs);
@@ -11606,6 +11773,7 @@ function buildAgenticWorkStatus(cwd = process.cwd()) {
11606
11773
  teamSync: {
11607
11774
  activeWorkCount: teamSyncSummary.activeWorkCount,
11608
11775
  staleWorkCount: teamSyncSummary.staleWorkCount,
11776
+ archivedWorkCount: teamSyncSummary.archivedWorkCount,
11609
11777
  handoffCount: teamSyncSummary.handoffCount,
11610
11778
  ...latestHandoff ? {
11611
11779
  latestHandoff: {
@@ -11656,6 +11824,7 @@ function printAgenticWorkStatus(status) {
11656
11824
  console.log(import_chalk5.default.bold("Team Sync"));
11657
11825
  console.log(`Active work: ${status.teamSync.activeWorkCount}`);
11658
11826
  console.log(`Stale work: ${status.teamSync.staleWorkCount}`);
11827
+ console.log(`Archived work: ${status.teamSync.archivedWorkCount}`);
11659
11828
  console.log(`Handoffs: ${status.teamSync.handoffCount}`);
11660
11829
  if (status.teamSync.latestHandoff) {
11661
11830
  console.log(`Latest handoff: ${toPreview(status.teamSync.latestHandoff.summary, 120)}`);
@@ -12068,7 +12237,7 @@ function printWorkflowTeamSyncResume(result) {
12068
12237
  }
12069
12238
  async function commitTaskMemory(options) {
12070
12239
  ensureConfigured();
12071
- const client = createClient(3e4);
12240
+ const client = createClient(TASK_COMMIT_TIMEOUT_MS);
12072
12241
  return client.endOfTaskCommit({
12073
12242
  summary: options.summary,
12074
12243
  category: options.category,
@@ -12076,6 +12245,101 @@ async function commitTaskMemory(options) {
12076
12245
  filesTouched: options.files
12077
12246
  });
12078
12247
  }
12248
+ function recordLocalFinalCommitHandoff(options) {
12249
+ const rootDir = process.cwd();
12250
+ autoArchiveTeamSyncState(rootDir);
12251
+ const state = loadTeamSyncState(rootDir);
12252
+ const record = buildTeamSyncHandoffRecord({
12253
+ summary: truncateText(options.summary, FINAL_COMMIT_SUMMARY_MAX_CHARS),
12254
+ files: options.files,
12255
+ attention: options.outcome === "completed" ? "watch" : "proof",
12256
+ next: options.outcome === "completed" ? "Review this local fallback handoff before starting follow-up work." : "Resolve the blocker captured in this local fallback handoff."
12257
+ });
12258
+ state.handoffs.push(record);
12259
+ state.updatedAt = record.createdAt;
12260
+ saveTeamSyncState(state, rootDir);
12261
+ return {
12262
+ status: "local_fallback",
12263
+ record_id: record.id,
12264
+ state_path: getTeamSyncStatePath(rootDir),
12265
+ category: "team_sync_handoff",
12266
+ source_session_id: "local-companion-fallback",
12267
+ files: record.files,
12268
+ error: options.error
12269
+ };
12270
+ }
12271
+ async function commitFinalTaskMemory(options) {
12272
+ ensureConfigured();
12273
+ const category = normalizeFinalCommitCategory(options.category);
12274
+ const attempts = [];
12275
+ const primarySummary = buildHostedFinalCommitSummary({
12276
+ workflowId: options.workflowId,
12277
+ summary: options.summary,
12278
+ maxLength: FINAL_COMMIT_SUMMARY_MAX_CHARS
12279
+ });
12280
+ const retrySummary = buildHostedFinalCommitSummary({
12281
+ workflowId: options.workflowId,
12282
+ summary: options.summary,
12283
+ maxLength: FINAL_COMMIT_RETRY_SUMMARY_MAX_CHARS
12284
+ });
12285
+ const callHosted = async (summary, timeoutMs) => {
12286
+ const client = createClient(timeoutMs);
12287
+ const handoffOnly = isFinalCommitCategory(category);
12288
+ return client.endOfTaskCommit({
12289
+ summary,
12290
+ category,
12291
+ outcome: options.outcome,
12292
+ filesTouched: options.files,
12293
+ persistTypes: handoffOnly ? [] : ["decision", "learning", "workflow"],
12294
+ handoffOnly
12295
+ });
12296
+ };
12297
+ try {
12298
+ return await callHosted(
12299
+ primarySummary,
12300
+ positiveIntegerEnv("SNIPARA_FINAL_COMMIT_TIMEOUT_MS", FINAL_COMMIT_TIMEOUT_MS)
12301
+ );
12302
+ } catch (error) {
12303
+ attempts.push({
12304
+ summary_chars: primarySummary.length,
12305
+ error: hostedCommitErrorMessage(error)
12306
+ });
12307
+ if (shouldRetryHostedFinalCommit(error)) {
12308
+ try {
12309
+ return await callHosted(
12310
+ retrySummary,
12311
+ positiveIntegerEnv("SNIPARA_FINAL_COMMIT_RETRY_TIMEOUT_MS", FINAL_COMMIT_RETRY_TIMEOUT_MS)
12312
+ );
12313
+ } catch (retryError) {
12314
+ attempts.push({
12315
+ summary_chars: retrySummary.length,
12316
+ error: hostedCommitErrorMessage(retryError)
12317
+ });
12318
+ }
12319
+ }
12320
+ }
12321
+ const lastError = attempts[attempts.length - 1]?.error ?? "hosted final-commit failed";
12322
+ const localHandoff = recordLocalFinalCommitHandoff({
12323
+ summary: options.summary,
12324
+ outcome: options.outcome,
12325
+ files: options.files,
12326
+ error: lastError
12327
+ });
12328
+ return {
12329
+ stored_count: 0,
12330
+ skipped_count: 0,
12331
+ candidates: [],
12332
+ stored_candidates: [],
12333
+ skipped_candidates: [],
12334
+ team_sync_handoff: localHandoff,
12335
+ hosted_final_commit: {
12336
+ status: "error",
12337
+ attempts,
12338
+ message: "Hosted snipara_end_of_task_commit failed; local workflow state and Team Sync fallback handoff were preserved."
12339
+ },
12340
+ message: "Hosted final-commit failed; local fallback handoff created"
12341
+ };
12342
+ }
12079
12343
  function printJournalWarning(result) {
12080
12344
  if (result?.status === "error" && result.error) {
12081
12345
  console.log(`Journal checkpoint: ${result.error}`);
@@ -12149,10 +12413,11 @@ async function finalCommitCommand(options) {
12149
12413
  });
12150
12414
  const state = readWorkflowState2();
12151
12415
  const outcome = options.outcome ?? "completed";
12152
- const summary = state ? [`Workflow ${state.workflowId}`, "Final commit", options.summary].join("\n") : options.summary;
12153
- const result = await commitTaskMemory({
12154
- summary,
12155
- category: options.category ?? "final-commit",
12416
+ const category = normalizeFinalCommitCategory(options.category);
12417
+ const result = await commitFinalTaskMemory({
12418
+ workflowId: state?.workflowId,
12419
+ summary: options.summary,
12420
+ category,
12156
12421
  outcome,
12157
12422
  files: options.files
12158
12423
  });
@@ -12162,7 +12427,7 @@ async function finalCommitCommand(options) {
12162
12427
  state.currentPhaseId = outcome === "completed" ? void 0 : state.currentPhaseId;
12163
12428
  state.updatedAt = now;
12164
12429
  state.lastCommit = {
12165
- category: options.category ?? "final-commit",
12430
+ category,
12166
12431
  outcome,
12167
12432
  summary: options.summary,
12168
12433
  committedAt: now
@@ -13171,6 +13436,14 @@ teamSync.command("complete-work").description("Close a local Team Sync work item
13171
13436
  json: options.json
13172
13437
  });
13173
13438
  });
13439
+ teamSync.command("sweep").description("Archive stale local Team Sync work items after an inactivity threshold").option("--days <days>", "Archive active work with no update after this many days", "14").option("--dry-run", "Preview which work items would be archived").option("-d, --dir <directory>", "Repository directory (default: current)").option("--json", "Print raw JSON").action(async (options) => {
13440
+ await teamSyncSweepCommand({
13441
+ days: options.days,
13442
+ dryRun: Boolean(options.dryRun),
13443
+ dir: options.dir,
13444
+ json: options.json
13445
+ });
13446
+ });
13174
13447
  teamSync.command("what-changed").description(
13175
13448
  "Summarize local Team Sync state and fetch hosted What Changed For Me when available"
13176
13449
  ).option("--since <date>", "Only include records created after this ISO date").option("-d, --dir <directory>", "Repository directory (default: current)").option(
@@ -13406,6 +13679,8 @@ if (require.main === module) {
13406
13679
  TEAM_SYNC_STATE_RELATIVE_PATH,
13407
13680
  WORKFLOW_PLANS_RELATIVE_DIR,
13408
13681
  WORKFLOW_STATE_RELATIVE_PATH,
13682
+ archiveInactiveTeamSyncWork,
13683
+ autoArchiveTeamSyncState,
13409
13684
  buildAgenticHandoffMarkdown,
13410
13685
  buildAgenticTimeline,
13411
13686
  buildAgenticWorkStatus,
@@ -13459,6 +13734,7 @@ if (require.main === module) {
13459
13734
  saveTeamSyncState,
13460
13735
  shouldSuggestOrchestratorForWorkflow,
13461
13736
  shouldSuggestRuntimeForWorkflow,
13737
+ teamSyncSweepCommand,
13462
13738
  verifyCommand,
13463
13739
  writeOrchestratorHandoff
13464
13740
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Snipara Git-style companion CLI for hosted context, agent continuity, hooks, and automation workflows",
5
5
  "main": "dist/index.js",
6
6
  "bin": {