@tenderprompt/cli 0.1.21 → 0.1.23

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.
@@ -0,0 +1,103 @@
1
+ export declare const ARTIFACT_MEMORY_SCHEMA = "tender.artifact-memory.v1";
2
+ export declare const ARTIFACT_MEMORY_NOTES_REF = "refs/notes/tender/memory";
3
+ export declare const ARTIFACT_MEMORY_NOTES_SHORT_REF = "tender/memory";
4
+ export type ArtifactMemoryKind = 'intent' | 'observation' | 'decision' | 'change' | 'validation' | 'failure' | 'hypothesis' | 'open_question' | 'procedure' | 'support_handoff' | 'reflection' | 'summary' | 'note';
5
+ export type ArtifactMemoryEntry = {
6
+ schema: typeof ARTIFACT_MEMORY_SCHEMA;
7
+ entryId: string;
8
+ sessionId: string;
9
+ artifactId: string;
10
+ tenantId?: string;
11
+ commitSha: string;
12
+ kind: ArtifactMemoryKind;
13
+ originalKind?: string;
14
+ visibility: 'support';
15
+ summary: string;
16
+ body?: string;
17
+ tags: string[];
18
+ status?: 'open' | 'resolved' | 'superseded';
19
+ severity?: 'info' | 'warning' | 'error';
20
+ source: {
21
+ actor: 'agent' | 'user' | 'cli';
22
+ command?: string;
23
+ };
24
+ related?: {
25
+ files?: string[];
26
+ commands?: string[];
27
+ previewUrl?: string;
28
+ publishedUrl?: string;
29
+ releaseId?: string;
30
+ issueUrl?: string;
31
+ };
32
+ safety: {
33
+ redacted: boolean;
34
+ warnings: string[];
35
+ };
36
+ createdAt: string;
37
+ };
38
+ export type ArtifactMemoryCommandRunner = (command: string, args: string[], options: {
39
+ cwd: string;
40
+ }) => Promise<{
41
+ exitCode: number;
42
+ stdout: string;
43
+ stderr: string;
44
+ }>;
45
+ export type ArtifactMemoryAppendInput = {
46
+ repoPath: string;
47
+ artifactId?: string | null;
48
+ tenantId?: string | null;
49
+ commitSha?: string | null;
50
+ kind: string;
51
+ summary: string;
52
+ body?: string | null;
53
+ tags?: string[];
54
+ status?: ArtifactMemoryEntry['status'];
55
+ severity?: ArtifactMemoryEntry['severity'];
56
+ source?: ArtifactMemoryEntry['source'];
57
+ related?: ArtifactMemoryEntry['related'];
58
+ now?: () => number;
59
+ runner: ArtifactMemoryCommandRunner;
60
+ };
61
+ export type ArtifactMemoryAppendResult = {
62
+ entry: ArtifactMemoryEntry;
63
+ warnings: string[];
64
+ noteBytes: number;
65
+ };
66
+ export type ArtifactMemorySyncResult = {
67
+ status: 'fetched' | 'pushed' | 'not_configured' | 'failed';
68
+ ref: typeof ARTIFACT_MEMORY_NOTES_REF;
69
+ commitSha: string | null;
70
+ error?: string;
71
+ };
72
+ export declare function normalizeArtifactMemoryKind(kind: string): {
73
+ kind: ArtifactMemoryKind;
74
+ originalKind?: string;
75
+ warnings: string[];
76
+ tags: string[];
77
+ };
78
+ export declare function redactArtifactMemoryText(value: string): {
79
+ text: string;
80
+ redacted: boolean;
81
+ warnings: string[];
82
+ };
83
+ export declare function readArtifactMemoryHead(input: {
84
+ repoPath: string;
85
+ runner: ArtifactMemoryCommandRunner;
86
+ }): Promise<string>;
87
+ export declare function appendArtifactMemoryNote(input: ArtifactMemoryAppendInput): Promise<ArtifactMemoryAppendResult>;
88
+ export declare function readArtifactMemoryEntries(input: {
89
+ repoPath: string;
90
+ runner: ArtifactMemoryCommandRunner;
91
+ commitSha?: string | null;
92
+ }): Promise<ArtifactMemoryEntry[]>;
93
+ export declare function fetchArtifactMemoryNotes(input: {
94
+ repoPath: string;
95
+ remoteName: string;
96
+ runner: ArtifactMemoryCommandRunner;
97
+ }): Promise<ArtifactMemorySyncResult>;
98
+ export declare function pushArtifactMemoryNotes(input: {
99
+ repoPath: string;
100
+ remoteName: string;
101
+ runner: ArtifactMemoryCommandRunner;
102
+ }): Promise<ArtifactMemorySyncResult>;
103
+ export declare function formatArtifactMemoryEntry(entry: ArtifactMemoryEntry): string;
@@ -0,0 +1,283 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export const ARTIFACT_MEMORY_SCHEMA = 'tender.artifact-memory.v1';
3
+ export const ARTIFACT_MEMORY_NOTES_REF = 'refs/notes/tender/memory';
4
+ export const ARTIFACT_MEMORY_NOTES_SHORT_REF = 'tender/memory';
5
+ const ARTIFACT_MEMORY_ENTRY_MAX_BYTES = 8 * 1024;
6
+ const ARTIFACT_MEMORY_NOTE_MAX_BYTES = 256 * 1024;
7
+ const ARTIFACT_MEMORY_SESSION_CONFIG_KEY = 'tender.memory.sessionId';
8
+ const ARTIFACT_MEMORY_KINDS = new Set([
9
+ 'intent',
10
+ 'observation',
11
+ 'decision',
12
+ 'change',
13
+ 'validation',
14
+ 'failure',
15
+ 'hypothesis',
16
+ 'open_question',
17
+ 'procedure',
18
+ 'support_handoff',
19
+ 'reflection',
20
+ 'summary',
21
+ 'note',
22
+ ]);
23
+ const KIND_ALIASES = new Map([
24
+ ['thought', 'reflection'],
25
+ ['debug_thought', 'reflection'],
26
+ ['debug-thought', 'reflection'],
27
+ ['error', 'failure'],
28
+ ['issue', 'failure'],
29
+ ['problem', 'failure'],
30
+ ['question', 'open_question'],
31
+ ['todo', 'open_question'],
32
+ ['final', 'summary'],
33
+ ]);
34
+ function byteLength(value) {
35
+ return new TextEncoder().encode(value).byteLength;
36
+ }
37
+ function normalizeTags(tags, fallback) {
38
+ const normalized = (tags ?? [])
39
+ .map((tag) => tag.trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, '-'))
40
+ .filter(Boolean)
41
+ .slice(0, 12);
42
+ return normalized.length > 0 ? [...new Set(normalized)] : fallback;
43
+ }
44
+ export function normalizeArtifactMemoryKind(kind) {
45
+ const normalized = kind.trim().toLowerCase().replaceAll('-', '_');
46
+ if (ARTIFACT_MEMORY_KINDS.has(normalized)) {
47
+ return {
48
+ kind: normalized,
49
+ warnings: [],
50
+ tags: [],
51
+ };
52
+ }
53
+ const alias = KIND_ALIASES.get(normalized);
54
+ if (alias && ARTIFACT_MEMORY_KINDS.has(alias)) {
55
+ return {
56
+ kind: alias,
57
+ originalKind: kind,
58
+ warnings: ['unknown_kind_normalized'],
59
+ tags: [],
60
+ };
61
+ }
62
+ return {
63
+ kind: 'note',
64
+ originalKind: kind,
65
+ warnings: ['unknown_kind_normalized'],
66
+ tags: ['uncategorized'],
67
+ };
68
+ }
69
+ const SECRET_PATTERNS = [
70
+ [/\b(authorization|proxy-authorization)\s*[:=]\s*(bearer\s+)?[^\s,;'"`]+/gi, '$1: [REDACTED]'],
71
+ [/\b(cookie|set-cookie)\s*[:=]\s*[^\n\r]+/gi, '$1: [REDACTED]'],
72
+ [/\b(x-api-key|api-key|private-token|x-shopify-access-token)\s*[:=]\s*[^\s,;'"`]+/gi, '$1: [REDACTED]'],
73
+ [/\b([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|ACCESS_KEY|PRIVATE_KEY))=([^\s]+)/g, '$1=[REDACTED]'],
74
+ [/\b(tp_[A-Za-z0-9._-]{12,})\b/g, '[REDACTED_TOKEN]'],
75
+ [/\b(gh[pousr]_[A-Za-z0-9_]{20,})\b/g, '[REDACTED_TOKEN]'],
76
+ [/\b(sk-[A-Za-z0-9_-]{20,})\b/g, '[REDACTED_TOKEN]'],
77
+ ];
78
+ export function redactArtifactMemoryText(value) {
79
+ let text = value;
80
+ const warnings = new Set();
81
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
82
+ const next = text.replace(pattern, replacement);
83
+ if (next !== text) {
84
+ warnings.add('secret_like_value_redacted');
85
+ text = next;
86
+ }
87
+ }
88
+ return {
89
+ text,
90
+ redacted: warnings.size > 0,
91
+ warnings: [...warnings],
92
+ };
93
+ }
94
+ function assertUsefulMemoryText(input) {
95
+ if (!input.summary.trim() && !input.body?.trim()) {
96
+ throw new Error('Memory entries require --summary or --body.\nNext: tender memory add --summary "What future agents should know"');
97
+ }
98
+ const total = `${input.summary}\n${input.body ?? ''}`.trim();
99
+ const lineCount = total.split(/\r?\n/).length;
100
+ if (lineCount > 240 || byteLength(total) > ARTIFACT_MEMORY_ENTRY_MAX_BYTES) {
101
+ throw new Error(`Memory entries are limited to ${ARTIFACT_MEMORY_ENTRY_MAX_BYTES} bytes. Summarize the relevant context instead.\nNext: tender memory handoff --reason "Need Tender support" --summary "Current blocker and what was tried"`);
102
+ }
103
+ if (lineCount > 40 && !/^summary|support_handoff|reflection$/i.test(input.summary)) {
104
+ throw new Error('Memory entries should be bounded summaries, not raw transcript dumps.\nNext: tender memory add --kind summary --summary "Compact the useful context" --body "Goal, changes, failures, and next step"');
105
+ }
106
+ }
107
+ async function runGit(input) {
108
+ const result = await input.runner('git', input.args, { cwd: input.repoPath });
109
+ if (!input.allowFailure && result.exitCode !== 0) {
110
+ throw new Error([`git ${input.args.join(' ')} failed with exit code ${result.exitCode}.`, result.stderr.trim() || result.stdout.trim()]
111
+ .filter(Boolean)
112
+ .join('\n'));
113
+ }
114
+ return result;
115
+ }
116
+ export async function readArtifactMemoryHead(input) {
117
+ const result = await runGit({
118
+ ...input,
119
+ args: ['rev-parse', 'HEAD'],
120
+ });
121
+ const commitSha = result.stdout.trim();
122
+ if (!/^[0-9a-f]{40}$/i.test(commitSha)) {
123
+ throw new Error('Artifact memory requires a checkout with a valid HEAD commit.\nNext: commit your source or run tender app init <app-id> --dir .');
124
+ }
125
+ return commitSha;
126
+ }
127
+ async function readArtifactMemorySessionId(input) {
128
+ const existing = await runGit({
129
+ ...input,
130
+ args: ['config', '--local', '--get', ARTIFACT_MEMORY_SESSION_CONFIG_KEY],
131
+ allowFailure: true,
132
+ });
133
+ const existingSessionId = existing.exitCode === 0 ? existing.stdout.trim() : '';
134
+ if (existingSessionId) {
135
+ return existingSessionId;
136
+ }
137
+ const sessionId = `sess_${randomUUID()}`;
138
+ await runGit({
139
+ ...input,
140
+ args: ['config', '--local', ARTIFACT_MEMORY_SESSION_CONFIG_KEY, sessionId],
141
+ });
142
+ return sessionId;
143
+ }
144
+ async function inferArtifactIdFromRemote(input) {
145
+ const result = await runGit({
146
+ ...input,
147
+ args: ['remote', '-v'],
148
+ allowFailure: true,
149
+ });
150
+ if (result.exitCode !== 0) {
151
+ return null;
152
+ }
153
+ const match = result.stdout.match(/\bartifact_[A-Za-z0-9_-]+(?=\.git|\b|\/)/);
154
+ return match?.[0] ?? null;
155
+ }
156
+ async function readExistingNote(input) {
157
+ const result = await runGit({
158
+ ...input,
159
+ args: ['notes', `--ref=${ARTIFACT_MEMORY_NOTES_SHORT_REF}`, 'show', input.commitSha],
160
+ allowFailure: true,
161
+ });
162
+ return result.exitCode === 0 ? result.stdout : '';
163
+ }
164
+ export async function appendArtifactMemoryNote(input) {
165
+ assertUsefulMemoryText(input);
166
+ const commitSha = input.commitSha ?? (await readArtifactMemoryHead(input));
167
+ const artifactId = input.artifactId ?? (await inferArtifactIdFromRemote(input));
168
+ if (!artifactId) {
169
+ throw new Error('Artifact memory requires an artifact id. Pass --artifact or run inside a checkout with a Tender artifact remote.\nNext: tender app git setup <app-id> --dir .');
170
+ }
171
+ const sessionId = await readArtifactMemorySessionId(input);
172
+ const normalizedKind = normalizeArtifactMemoryKind(input.kind);
173
+ const redactedSummary = redactArtifactMemoryText(input.summary.trim());
174
+ const redactedBody = input.body?.trim() ? redactArtifactMemoryText(input.body.trim()) : null;
175
+ const warnings = [
176
+ ...normalizedKind.warnings,
177
+ ...redactedSummary.warnings,
178
+ ...(redactedBody?.warnings ?? []),
179
+ ];
180
+ const entry = {
181
+ schema: ARTIFACT_MEMORY_SCHEMA,
182
+ entryId: `mem_${randomUUID()}`,
183
+ sessionId,
184
+ artifactId,
185
+ ...(input.tenantId ? { tenantId: input.tenantId } : {}),
186
+ commitSha,
187
+ kind: normalizedKind.kind,
188
+ ...(normalizedKind.originalKind ? { originalKind: normalizedKind.originalKind } : {}),
189
+ visibility: 'support',
190
+ summary: redactedSummary.text,
191
+ ...(redactedBody?.text ? { body: redactedBody.text } : {}),
192
+ tags: normalizeTags(input.tags, normalizedKind.tags),
193
+ ...(input.status ? { status: input.status } : {}),
194
+ ...(input.severity ? { severity: input.severity } : {}),
195
+ source: input.source ?? { actor: 'agent' },
196
+ ...(input.related && Object.keys(input.related).length > 0 ? { related: input.related } : {}),
197
+ safety: {
198
+ redacted: redactedSummary.redacted || Boolean(redactedBody?.redacted),
199
+ warnings: [...new Set(warnings)],
200
+ },
201
+ createdAt: new Date(input.now?.() ?? Date.now()).toISOString(),
202
+ };
203
+ const line = JSON.stringify(entry);
204
+ if (byteLength(line) > ARTIFACT_MEMORY_ENTRY_MAX_BYTES) {
205
+ throw new Error(`Memory entry is limited to ${ARTIFACT_MEMORY_ENTRY_MAX_BYTES} bytes after redaction.\nNext: tender memory add --kind summary --summary "Short context" --body "Bounded details only"`);
206
+ }
207
+ const existingNote = await readExistingNote({ ...input, commitSha });
208
+ const nextNote = `${existingNote.trimEnd()}${existingNote.trim() ? '\n' : ''}${line}\n`;
209
+ const noteBytes = byteLength(nextNote);
210
+ const noteWarnings = noteBytes > ARTIFACT_MEMORY_NOTE_MAX_BYTES ? ['commit_note_payload_large_compaction_recommended'] : [];
211
+ await runGit({
212
+ ...input,
213
+ args: ['notes', `--ref=${ARTIFACT_MEMORY_NOTES_SHORT_REF}`, 'add', '-f', '-m', nextNote, commitSha],
214
+ });
215
+ return {
216
+ entry: noteWarnings.length > 0 ? { ...entry, safety: { ...entry.safety, warnings: [...entry.safety.warnings, ...noteWarnings] } } : entry,
217
+ warnings: [...new Set([...warnings, ...noteWarnings])],
218
+ noteBytes,
219
+ };
220
+ }
221
+ export async function readArtifactMemoryEntries(input) {
222
+ const commitSha = input.commitSha ?? (await readArtifactMemoryHead(input));
223
+ const note = await readExistingNote({ ...input, commitSha });
224
+ return note
225
+ .split(/\r?\n/)
226
+ .map((line) => line.trim())
227
+ .filter(Boolean)
228
+ .flatMap((line) => {
229
+ try {
230
+ const value = JSON.parse(line);
231
+ return value?.schema === ARTIFACT_MEMORY_SCHEMA ? [value] : [];
232
+ }
233
+ catch {
234
+ return [];
235
+ }
236
+ });
237
+ }
238
+ export async function fetchArtifactMemoryNotes(input) {
239
+ const result = await runGit({
240
+ ...input,
241
+ args: ['fetch', input.remoteName, `${ARTIFACT_MEMORY_NOTES_REF}:${ARTIFACT_MEMORY_NOTES_REF}`],
242
+ allowFailure: true,
243
+ });
244
+ if (result.exitCode !== 0) {
245
+ return {
246
+ status: 'failed',
247
+ ref: ARTIFACT_MEMORY_NOTES_REF,
248
+ commitSha: null,
249
+ error: result.stderr.trim() || result.stdout.trim() || 'git fetch notes failed',
250
+ };
251
+ }
252
+ return { status: 'fetched', ref: ARTIFACT_MEMORY_NOTES_REF, commitSha: null };
253
+ }
254
+ export async function pushArtifactMemoryNotes(input) {
255
+ const head = await readArtifactMemoryHead(input).catch(() => null);
256
+ const remote = await runGit({
257
+ ...input,
258
+ args: ['remote', 'get-url', input.remoteName],
259
+ allowFailure: true,
260
+ });
261
+ if (remote.exitCode !== 0) {
262
+ return { status: 'not_configured', ref: ARTIFACT_MEMORY_NOTES_REF, commitSha: head };
263
+ }
264
+ const result = await runGit({
265
+ ...input,
266
+ args: ['push', input.remoteName, `${ARTIFACT_MEMORY_NOTES_REF}:${ARTIFACT_MEMORY_NOTES_REF}`],
267
+ allowFailure: true,
268
+ });
269
+ if (result.exitCode !== 0) {
270
+ return {
271
+ status: 'failed',
272
+ ref: ARTIFACT_MEMORY_NOTES_REF,
273
+ commitSha: head,
274
+ error: result.stderr.trim() || result.stdout.trim() || 'git push notes failed',
275
+ };
276
+ }
277
+ return { status: 'pushed', ref: ARTIFACT_MEMORY_NOTES_REF, commitSha: head };
278
+ }
279
+ export function formatArtifactMemoryEntry(entry) {
280
+ const tags = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : '';
281
+ const status = entry.status ? ` ${entry.status}` : '';
282
+ return `${entry.createdAt} ${entry.kind}${status}: ${entry.summary}${tags}`;
283
+ }
package/dist/index.js CHANGED
@@ -6,12 +6,13 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { generateTenderAppEnvTypes, isForbiddenTenderAppRepoPath, isForbiddenTenderAppSecretPath, validateTenderApp, } from '@tenderprompt/tender-app-validator';
9
+ import { ARTIFACT_MEMORY_NOTES_REF, appendArtifactMemoryNote, fetchArtifactMemoryNotes, formatArtifactMemoryEntry, pushArtifactMemoryNotes, readArtifactMemoryEntries, readArtifactMemoryHead, } from "./artifact-memory.js";
9
10
  import { collectTenderAppFiles } from "./local-files.js";
10
11
  const CLI_PACKAGE_NAME = '@tenderprompt/cli';
11
12
  const UNKNOWN_CLI_VERSION = '0.0.0';
12
13
  const RECOMMENDED_TENDER_PROMPT_SKILL = {
13
14
  name: 'tender-prompt',
14
- version: '0.1.4',
15
+ version: '0.1.5',
15
16
  source: 'git@github.com:tenderprompt/skills.git',
16
17
  path: 'skills/tender-prompt/SKILL.md',
17
18
  updateHint: 'Refresh the tender-prompt skill from git@github.com:tenderprompt/skills.git when the local skill version is older.',
@@ -79,6 +80,10 @@ Commands:
79
80
  tender auth login --device
80
81
  Authorize this machine with a browser-approved device flow
81
82
  tender auth status Show whether a Tender API token is available
83
+ tender memory add Add artifact memory to Git notes for future agents and support
84
+ tender memory handoff Capture a support handoff memory entry
85
+ tender memory brief Print the latest useful artifact memory
86
+ tender memory flush Push artifact memory notes to the Tender Git remote
82
87
  tender playbooks list List remote Tender Prompt playbook metadata
83
88
  tender playbooks get <id> Fetch one remote playbook body on demand
84
89
  tender playbooks file <id> --path <path>
@@ -143,6 +148,10 @@ Examples:
143
148
  tender capabilities --json
144
149
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
145
150
  tender auth status --json
151
+ tender memory add --kind decision --summary "Kept auth in the host gateway" --json
152
+ tender memory handoff --reason "Preview publish is blocked" --summary "Need Tender support" --json
153
+ tender memory brief --json
154
+ tender memory flush --json
146
155
  tender playbooks list --json
147
156
  tender playbooks get recharge-bundle-builder --json
148
157
  tender playbooks file recharge-bundle-builder --path references/recharge-payloads.md --json
@@ -219,6 +228,52 @@ Examples:
219
228
  tender auth status
220
229
  tender auth status --profile publish --json`;
221
230
  }
231
+ function memoryHelp() {
232
+ return `Usage: tender memory <command> [options]
233
+
234
+ Commands:
235
+ tender memory start --summary <text>
236
+ Start an artifact memory session. Optional; sessions are created lazily.
237
+ tender memory add --kind <kind> --summary <text>
238
+ Add one bounded artifact memory entry to Git notes.
239
+ tender memory handoff Capture a support-focused handoff entry.
240
+ tender memory show Show memory entries attached to the current commit.
241
+ tender memory brief Show the latest useful memory summary.
242
+ tender memory flush Push refs/notes/tender/memory to the configured remote.
243
+ tender memory export Export artifact memory entries as JSONL.
244
+
245
+ Options:
246
+ --dir <path> Artifact checkout directory. Defaults to current directory.
247
+ --artifact <id> Artifact id when it cannot be inferred from the remote.
248
+ --remote <name> Git remote to fetch or push notes. Defaults to tender.
249
+ --json Emit stable JSON for coding agents.
250
+
251
+ Examples:
252
+ tender memory add --kind intent --summary "Fix checkout submit flow" --json
253
+ tender memory add --kind decision --summary "Kept credential injection in the host gateway" --body "Generated code should not set Authorization headers." --json
254
+ tender memory handoff --reason "Need Tender support debugging publish failure" --summary "Preview works but publish fails" --json
255
+ tender memory brief --json
256
+ tender memory export --format jsonl
257
+ tender memory flush --json`;
258
+ }
259
+ function memoryAddHelp() {
260
+ return `Usage: tender memory add --kind <kind> --summary <text> [--body <text>] [--tag <tag>] [--dir <path>] [--artifact <id>] [--remote <name>] [--json]
261
+
262
+ Known kinds: intent, observation, decision, change, validation, failure, hypothesis, open_question, procedure, support_handoff, reflection, summary, note.
263
+ Unknown kinds are stored as kind "note" with originalKind preserved.
264
+
265
+ Examples:
266
+ tender memory add --kind observation --summary "The app reads product.metadata.heroVideoUrl" --json
267
+ tender memory add --kind summary --summary "Session summary" --body "Changed src/server.ts, validated, and previewed." --json`;
268
+ }
269
+ function memoryHandoffHelp() {
270
+ return `Usage: tender memory handoff [--reason <text>] [--summary <text>] [--body <text>] [--dir <path>] [--artifact <id>] [--remote <name>] [--json]
271
+
272
+ Use this when the user wants to ask Tender for help. If --reason is omitted, the CLI tries to infer context from recent failure or open_question entries.
273
+
274
+ Examples:
275
+ tender memory handoff --reason "Need Tender support debugging preview failure" --summary "Preview route /api/vote returns 500" --json`;
276
+ }
222
277
  function playbooksHelp() {
223
278
  return `Usage: tender playbooks <command> [options]
224
279
 
@@ -1087,6 +1142,7 @@ function tenderCliCapabilities() {
1087
1142
  'tender --version',
1088
1143
  'tender --help',
1089
1144
  'tender auth --help',
1145
+ 'tender memory --help',
1090
1146
  'tender playbooks --help',
1091
1147
  'tender app --help',
1092
1148
  'tender app agent heartbeat --help',
@@ -1163,6 +1219,7 @@ function tenderCliCapabilities() {
1163
1219
  when: 'When editing app source from a local checkout.',
1164
1220
  commands: [
1165
1221
  'tender app agent heartbeat <artifact-id> --source codex --status connected --summary "Loaded Tender Prompt context" --json',
1222
+ 'tender memory add --artifact <artifact-id> --kind intent --summary "User goal and current plan" --json',
1166
1223
  'tender app doctor --dir <dir> --json',
1167
1224
  'tender app agent heartbeat <artifact-id> --source codex --status working --phase editing --summary "Editing app files" --json',
1168
1225
  'tender app git setup <artifact-id> --dir <dir> --dry-run --json',
@@ -1172,9 +1229,13 @@ function tenderCliCapabilities() {
1172
1229
  'tender app outbound-auth list <artifact-id> --json',
1173
1230
  'tender app publish <artifact-id> --dry-run --json',
1174
1231
  'tender app publish <artifact-id> --commit $(git rev-parse HEAD) --confirm publish --stream --json',
1232
+ 'tender memory add --kind summary --summary "What changed, what passed, and what remains" --json',
1233
+ 'tender memory flush --json',
1175
1234
  ],
1176
1235
  notes: [
1177
1236
  'Send heartbeat updates only at meaningful milestones or when waiting for user input; do not send rapid polling updates.',
1237
+ 'Use tender memory add for durable artifact context that future agents or Tender support should know.',
1238
+ 'Use tender memory handoff when blocked or when the user asks to contact Tender support.',
1178
1239
  'If preview or publish reports outbound auth grant approval required, run tender app outbound-auth approve <artifact-id> --all --confirm approve --json, then retry.',
1179
1240
  'Publishing is production-facing and requires explicit user intent.',
1180
1241
  ],
@@ -3151,6 +3212,201 @@ function parseContextRefreshArgs(args) {
3151
3212
  json,
3152
3213
  };
3153
3214
  }
3215
+ function parseMemoryCommonArgs(args) {
3216
+ let dir = '.';
3217
+ let artifactId = null;
3218
+ let remoteName = 'tender';
3219
+ let json = false;
3220
+ const remaining = [];
3221
+ for (let index = 0; index < args.length; index += 1) {
3222
+ const arg = args[index];
3223
+ if (arg === '--json') {
3224
+ json = true;
3225
+ continue;
3226
+ }
3227
+ if (arg === '--dir') {
3228
+ dir = parseRequiredFlagValue(args[index + 1], '--dir');
3229
+ index += 1;
3230
+ continue;
3231
+ }
3232
+ if (arg === '--artifact') {
3233
+ artifactId = parseRequiredFlagValue(args[index + 1], '--artifact');
3234
+ index += 1;
3235
+ continue;
3236
+ }
3237
+ if (arg === '--remote') {
3238
+ remoteName = parseRemoteName(args[index + 1]);
3239
+ index += 1;
3240
+ continue;
3241
+ }
3242
+ remaining.push(arg);
3243
+ }
3244
+ return { dir, artifactId, remoteName, json, remaining };
3245
+ }
3246
+ function parseMemoryStartArgs(args) {
3247
+ if (args.includes('--help') || args.includes('-h')) {
3248
+ return { command: 'help', topic: 'memory start' };
3249
+ }
3250
+ const parsed = parseMemoryCommonArgs(args);
3251
+ let summary = '';
3252
+ let body = null;
3253
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3254
+ const arg = parsed.remaining[index];
3255
+ if (arg === '--summary') {
3256
+ summary = parseRequiredFlagValue(parsed.remaining[index + 1], '--summary');
3257
+ index += 1;
3258
+ continue;
3259
+ }
3260
+ if (arg === '--body') {
3261
+ body = parseRequiredFlagValue(parsed.remaining[index + 1], '--body');
3262
+ index += 1;
3263
+ continue;
3264
+ }
3265
+ throw new TenderCliUsageError(`Unknown memory start option: ${arg}`);
3266
+ }
3267
+ if (!summary.trim()) {
3268
+ throw new TenderCliUsageError('memory start requires --summary.\nNext: tender memory start --summary "Current user goal"');
3269
+ }
3270
+ return { command: 'memory start', summary, body, ...parsed };
3271
+ }
3272
+ function parseMemoryAddArgs(args) {
3273
+ if (args.includes('--help') || args.includes('-h')) {
3274
+ return { command: 'help', topic: 'memory add' };
3275
+ }
3276
+ const parsed = parseMemoryCommonArgs(args);
3277
+ let kind = 'note';
3278
+ let summary = '';
3279
+ let body = null;
3280
+ const tags = [];
3281
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3282
+ const arg = parsed.remaining[index];
3283
+ if (arg === '--kind') {
3284
+ kind = parseRequiredFlagValue(parsed.remaining[index + 1], '--kind');
3285
+ index += 1;
3286
+ continue;
3287
+ }
3288
+ if (arg === '--summary') {
3289
+ summary = parseRequiredFlagValue(parsed.remaining[index + 1], '--summary');
3290
+ index += 1;
3291
+ continue;
3292
+ }
3293
+ if (arg === '--body') {
3294
+ body = parseRequiredFlagValue(parsed.remaining[index + 1], '--body');
3295
+ index += 1;
3296
+ continue;
3297
+ }
3298
+ if (arg === '--tag') {
3299
+ tags.push(parseRequiredFlagValue(parsed.remaining[index + 1], '--tag'));
3300
+ index += 1;
3301
+ continue;
3302
+ }
3303
+ throw new TenderCliUsageError(`Unknown memory add option: ${arg}`);
3304
+ }
3305
+ return { command: 'memory add', kind, summary, body, tags, ...parsed };
3306
+ }
3307
+ function parseMemoryHandoffArgs(args) {
3308
+ if (args.includes('--help') || args.includes('-h')) {
3309
+ return { command: 'help', topic: 'memory handoff' };
3310
+ }
3311
+ const parsed = parseMemoryCommonArgs(args);
3312
+ let reason = null;
3313
+ let summary = null;
3314
+ let body = null;
3315
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3316
+ const arg = parsed.remaining[index];
3317
+ if (arg === '--reason') {
3318
+ reason = parseRequiredFlagValue(parsed.remaining[index + 1], '--reason');
3319
+ index += 1;
3320
+ continue;
3321
+ }
3322
+ if (arg === '--summary') {
3323
+ summary = parseRequiredFlagValue(parsed.remaining[index + 1], '--summary');
3324
+ index += 1;
3325
+ continue;
3326
+ }
3327
+ if (arg === '--body') {
3328
+ body = parseRequiredFlagValue(parsed.remaining[index + 1], '--body');
3329
+ index += 1;
3330
+ continue;
3331
+ }
3332
+ throw new TenderCliUsageError(`Unknown memory handoff option: ${arg}`);
3333
+ }
3334
+ return { command: 'memory handoff', reason, summary, body, ...parsed };
3335
+ }
3336
+ function parseMemoryShowArgs(args) {
3337
+ if (args.includes('--help') || args.includes('-h')) {
3338
+ return { command: 'help', topic: 'memory show' };
3339
+ }
3340
+ const parsed = parseMemoryCommonArgs(args);
3341
+ let commitSha = null;
3342
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3343
+ const arg = parsed.remaining[index];
3344
+ if (arg === '--commit') {
3345
+ commitSha = parseRequiredFlagValue(parsed.remaining[index + 1], '--commit');
3346
+ index += 1;
3347
+ continue;
3348
+ }
3349
+ throw new TenderCliUsageError(`Unknown memory show option: ${arg}`);
3350
+ }
3351
+ return { command: 'memory show', commitSha, ...parsed };
3352
+ }
3353
+ function parseMemoryBriefArgs(args) {
3354
+ if (args.includes('--help') || args.includes('-h')) {
3355
+ return { command: 'help', topic: 'memory brief' };
3356
+ }
3357
+ const parsed = parseMemoryCommonArgs(args);
3358
+ let limit = 12;
3359
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3360
+ const arg = parsed.remaining[index];
3361
+ if (arg === '--limit') {
3362
+ limit = parsePositiveIntegerFlag(parsed.remaining[index + 1], '--limit');
3363
+ index += 1;
3364
+ continue;
3365
+ }
3366
+ throw new TenderCliUsageError(`Unknown memory brief option: ${arg}`);
3367
+ }
3368
+ return { command: 'memory brief', limit, ...parsed };
3369
+ }
3370
+ function parseMemoryFlushArgs(args) {
3371
+ if (args.includes('--help') || args.includes('-h')) {
3372
+ return { command: 'help', topic: 'memory flush' };
3373
+ }
3374
+ const parsed = parseMemoryCommonArgs(args);
3375
+ if (parsed.remaining.length > 0) {
3376
+ throw new TenderCliUsageError(`Unknown memory flush option: ${parsed.remaining[0]}`);
3377
+ }
3378
+ return { command: 'memory flush', ...parsed };
3379
+ }
3380
+ function parseMemoryExportArgs(args) {
3381
+ if (args.includes('--help') || args.includes('-h')) {
3382
+ return { command: 'help', topic: 'memory export' };
3383
+ }
3384
+ const parsed = parseMemoryCommonArgs(args);
3385
+ let format = null;
3386
+ let commitSha = null;
3387
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3388
+ const arg = parsed.remaining[index];
3389
+ if (arg === '--format') {
3390
+ const value = parseRequiredFlagValue(parsed.remaining[index + 1], '--format');
3391
+ if (value !== 'jsonl') {
3392
+ throw new TenderCliUsageError(`Unsupported memory export format: ${value}.\nNext: tender memory export --format jsonl --dir ${parsed.dir}`);
3393
+ }
3394
+ format = value;
3395
+ index += 1;
3396
+ continue;
3397
+ }
3398
+ if (arg === '--commit') {
3399
+ commitSha = parseRequiredFlagValue(parsed.remaining[index + 1], '--commit');
3400
+ index += 1;
3401
+ continue;
3402
+ }
3403
+ throw new TenderCliUsageError(`Unknown memory export option: ${arg}`);
3404
+ }
3405
+ if (format === null) {
3406
+ throw new TenderCliUsageError(`memory export requires --format jsonl.\nNext: tender memory export --format jsonl --dir ${parsed.dir}`);
3407
+ }
3408
+ return { command: 'memory export', format, commitSha, ...parsed };
3409
+ }
3154
3410
  function parseTokenProfile(value) {
3155
3411
  if (value === 'edit-preview' || value === 'publish' || value === 'create' || value === 'db-read') {
3156
3412
  return value;
@@ -4848,6 +5104,33 @@ function parseTenderArgs(args) {
4848
5104
  }
4849
5105
  throw new TenderCliUsageError(`Unknown auth command: ${args[1]}`);
4850
5106
  }
5107
+ if (args[0] === 'memory') {
5108
+ if (args[1] === undefined || args[1] === '--help' || args[1] === '-h') {
5109
+ return { command: 'help', topic: 'memory' };
5110
+ }
5111
+ if (args[1] === 'start') {
5112
+ return parseMemoryStartArgs(args.slice(2));
5113
+ }
5114
+ if (args[1] === 'add') {
5115
+ return parseMemoryAddArgs(args.slice(2));
5116
+ }
5117
+ if (args[1] === 'handoff') {
5118
+ return parseMemoryHandoffArgs(args.slice(2));
5119
+ }
5120
+ if (args[1] === 'show') {
5121
+ return parseMemoryShowArgs(args.slice(2));
5122
+ }
5123
+ if (args[1] === 'brief') {
5124
+ return parseMemoryBriefArgs(args.slice(2));
5125
+ }
5126
+ if (args[1] === 'flush') {
5127
+ return parseMemoryFlushArgs(args.slice(2));
5128
+ }
5129
+ if (args[1] === 'export') {
5130
+ return parseMemoryExportArgs(args.slice(2));
5131
+ }
5132
+ throw new TenderCliUsageError(`Unknown memory command: ${args[1]}`);
5133
+ }
4851
5134
  if (args[0] === 'playbooks') {
4852
5135
  if (args[1] === undefined || args[1] === '--help' || args[1] === '-h') {
4853
5136
  return { command: 'help', topic: 'playbooks' };
@@ -7212,6 +7495,13 @@ async function runGitSetup(command, io, runtime) {
7212
7495
  credentialHelper: helper,
7213
7496
  });
7214
7497
  }
7498
+ const memoryFetch = command.dryRun
7499
+ ? { status: 'not_configured', ref: ARTIFACT_MEMORY_NOTES_REF, commitSha: null }
7500
+ : await fetchArtifactMemoryNotes({
7501
+ repoPath: root,
7502
+ remoteName: command.remoteName,
7503
+ runner: commandRunnerForMemory(runtime),
7504
+ });
7215
7505
  const result = {
7216
7506
  ok: true,
7217
7507
  command: command.command,
@@ -7226,6 +7516,7 @@ async function runGitSetup(command, io, runtime) {
7226
7516
  authSource: credentials.source,
7227
7517
  profile: credentials.profileName,
7228
7518
  credentialHelper: helper,
7519
+ memoryFetch,
7229
7520
  commands: plannedCommands.map((parts) => parts.join(' ')),
7230
7521
  next: `git push ${command.remoteName} ${git.defaultBranch}`,
7231
7522
  };
@@ -7237,6 +7528,7 @@ async function runGitSetup(command, io, runtime) {
7237
7528
  `remote: ${command.remoteName}`,
7238
7529
  `remote_url: ${git.remoteUrl}`,
7239
7530
  `default_branch: ${git.defaultBranch}`,
7531
+ `memory_fetch: ${memoryFetch.status}`,
7240
7532
  `next: ${result.next}`,
7241
7533
  ].join('\n'));
7242
7534
  }
@@ -7368,6 +7660,14 @@ async function runArtifactsInit(command, io, runtime) {
7368
7660
  job: null,
7369
7661
  result: null,
7370
7662
  };
7663
+ let memoryFetch = {
7664
+ status: command.dryRun ? 'not_configured' : 'failed',
7665
+ ref: ARTIFACT_MEMORY_NOTES_REF,
7666
+ commitSha: null,
7667
+ ...(command.dryRun ? {} : { error: 'not_attempted' }),
7668
+ };
7669
+ let memory = null;
7670
+ let memorySync = null;
7371
7671
  let seedPaths = [];
7372
7672
  let plannedFiles = await planContextFiles({
7373
7673
  root,
@@ -7396,6 +7696,11 @@ async function runArtifactsInit(command, io, runtime) {
7396
7696
  remoteUrl: git.remoteUrl,
7397
7697
  credentialHelper: helper,
7398
7698
  });
7699
+ memoryFetch = await fetchArtifactMemoryNotes({
7700
+ repoPath: root,
7701
+ remoteName: command.remoteName,
7702
+ runner: commandRunnerForMemory(runtime),
7703
+ });
7399
7704
  sourceCheckout = await maybeCheckoutInitialArtifactSource({
7400
7705
  runtime,
7401
7706
  root,
@@ -7637,6 +7942,22 @@ async function runArtifactsInit(command, io, runtime) {
7637
7942
  };
7638
7943
  }
7639
7944
  }
7945
+ memory = await appendAutomaticMemory({
7946
+ repoPath: root,
7947
+ artifactId: command.artifactId,
7948
+ kind: 'change',
7949
+ summary: 'Initialized local Tender app checkout',
7950
+ body: `Source checkout: ${sourceCheckout.skippedReason ?? (sourceCheckout.checkedOut ? 'checked_out' : 'not_checked_out')}\nSource export: ${sourceExport.skippedReason ?? sourceExport.source ?? 'none'}\nPreview: ${preview.skippedReason ?? (preview.attempted ? 'requested' : 'not_requested')}`,
7951
+ tags: ['init'],
7952
+ status: 'resolved',
7953
+ related: {
7954
+ files: seedPaths,
7955
+ commands: [command.command],
7956
+ ...(preview.job?.jobId ? { releaseId: preview.job.jobId } : {}),
7957
+ },
7958
+ runtime,
7959
+ });
7960
+ memorySync = await flushAutomaticMemory({ repoPath: root, remoteName: command.remoteName, runtime });
7640
7961
  }
7641
7962
  const result = {
7642
7963
  ok: true,
@@ -7657,6 +7978,9 @@ async function runArtifactsInit(command, io, runtime) {
7657
7978
  sourceExport,
7658
7979
  seedCommit,
7659
7980
  preview,
7981
+ memoryFetch,
7982
+ memory,
7983
+ memorySync,
7660
7984
  summary: {
7661
7985
  create: plannedFiles.filter((file) => file.action === 'create').length,
7662
7986
  overwrite: plannedFiles.filter((file) => file.action === 'overwrite').length,
@@ -7687,6 +8011,8 @@ async function runArtifactsInit(command, io, runtime) {
7687
8011
  `dir: ${root}`,
7688
8012
  `remote: ${command.remoteName}`,
7689
8013
  preview.attempted && preview.job?.jobId ? `preview_job: ${preview.job.jobId}` : null,
8014
+ memorySync ? `memory_sync: ${memorySync.status}` : null,
8015
+ memorySync?.status === 'failed' ? `next: tender memory flush --dir ${command.dir}` : null,
7690
8016
  `next: ${result.next}`,
7691
8017
  ]
7692
8018
  .filter((line) => Boolean(line))
@@ -7729,6 +8055,26 @@ async function postLifecycle(input) {
7729
8055
  io: input.io,
7730
8056
  json: input.command.json,
7731
8057
  });
8058
+ await appendAutomaticMemory({
8059
+ repoPath: process.cwd(),
8060
+ artifactId: input.command.artifactId,
8061
+ kind: input.operation === 'publish' ? 'change' : 'validation',
8062
+ summary: `${input.command.command} streamed`,
8063
+ body: `Operation: ${input.operation}\nStatus: streamed`,
8064
+ tags: [input.operation, 'stream'],
8065
+ status: 'resolved',
8066
+ related: {
8067
+ commands: [input.command.command],
8068
+ ...('commitSha' in input.command && input.command.commitSha ? { releaseId: input.command.commitSha } : {}),
8069
+ },
8070
+ runtime: input.runtime,
8071
+ });
8072
+ if (input.operation === 'publish') {
8073
+ const memorySync = await flushAutomaticMemory({ repoPath: process.cwd(), remoteName: 'tender', runtime: input.runtime });
8074
+ if (memorySync.status === 'failed') {
8075
+ input.io.stderr(`Memory notes sync ${memorySync.status}. Next: tender memory flush --dir .`);
8076
+ }
8077
+ }
7732
8078
  return 0;
7733
8079
  }
7734
8080
  const payload = await parseJsonResponse(response);
@@ -7747,8 +8093,36 @@ async function postLifecycle(input) {
7747
8093
  job: payload.job ?? null,
7748
8094
  result: payload.result ?? null,
7749
8095
  };
8096
+ const repoPath = process.cwd();
8097
+ const changedFiles = await readLocalChangedFiles({ runtime: input.runtime, repoPath });
8098
+ const releaseGitRef = input.operation === 'publish' ? readReleaseGitRefFromResult(result.result) : null;
8099
+ const memory = await appendAutomaticMemory({
8100
+ repoPath,
8101
+ artifactId: input.command.artifactId,
8102
+ kind: input.operation === 'validate' ? 'validation' : input.operation === 'publish' ? 'change' : 'validation',
8103
+ summary: `${input.command.command} completed`,
8104
+ body: `Operation: ${result.operation}\nStatus: completed`,
8105
+ tags: [input.operation],
8106
+ status: 'resolved',
8107
+ related: {
8108
+ files: changedFiles,
8109
+ commands: [input.command.command],
8110
+ ...(input.operation === 'preview' ? { previewUrl: readPreviewUrlFromJob(result.job) ?? undefined } : {}),
8111
+ ...(input.operation === 'publish' ? { publishedUrl: readObjectString(readObject(result.result), 'publishedUrl') ?? undefined } : {}),
8112
+ ...(releaseGitRef?.tagName ? { releaseId: releaseGitRef.tagName } : {}),
8113
+ },
8114
+ runtime: input.runtime,
8115
+ });
8116
+ const memorySync = input.operation === 'publish'
8117
+ ? await flushAutomaticMemory({ repoPath, remoteName: 'tender', runtime: input.runtime })
8118
+ : null;
8119
+ const resultWithMemory = {
8120
+ ...result,
8121
+ memory,
8122
+ ...(memorySync ? { memorySync } : {}),
8123
+ };
7750
8124
  if (input.command.json) {
7751
- input.io.stdout(JSON.stringify(result, null, 2));
8125
+ input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
7752
8126
  }
7753
8127
  else {
7754
8128
  input.io.stdout([
@@ -7756,6 +8130,8 @@ async function postLifecycle(input) {
7756
8130
  `operation: ${result.operation}`,
7757
8131
  'status: completed',
7758
8132
  ...(input.operation === 'publish' ? formatPublishResultLines(result.result) : []),
8133
+ memorySync ? `memory_sync: ${memorySync.status}` : null,
8134
+ memorySync?.status === 'failed' ? `next: tender memory flush --dir .` : null,
7759
8135
  ].join('\n'));
7760
8136
  }
7761
8137
  return 0;
@@ -8337,6 +8713,283 @@ async function runGitRemote(command, io, runtime) {
8337
8713
  }
8338
8714
  return 0;
8339
8715
  }
8716
+ function commandRunnerForMemory(runtime) {
8717
+ return runtime.commandRunner ?? defaultCommandRunner;
8718
+ }
8719
+ function memoryRecoveryForArtifact(artifactId, dir) {
8720
+ return artifactId ? `tender app git setup ${artifactId} --dir ${dir}` : 'tender app git setup <app-id> --dir .';
8721
+ }
8722
+ function formatMemoryAppendResult(result) {
8723
+ return {
8724
+ entry: result.entry,
8725
+ warnings: result.warnings,
8726
+ noteBytes: result.noteBytes,
8727
+ ref: ARTIFACT_MEMORY_NOTES_REF,
8728
+ };
8729
+ }
8730
+ async function appendCliMemoryEntry(input) {
8731
+ const repoPath = path.resolve(input.command.dir);
8732
+ try {
8733
+ return await appendArtifactMemoryNote({
8734
+ repoPath,
8735
+ artifactId: input.command.artifactId,
8736
+ kind: input.kind,
8737
+ summary: input.summary,
8738
+ body: input.body,
8739
+ tags: input.tags,
8740
+ status: input.status,
8741
+ severity: input.severity,
8742
+ source: {
8743
+ actor: 'agent',
8744
+ command: input.command.command,
8745
+ },
8746
+ runner: commandRunnerForMemory(input.runtime),
8747
+ now: input.runtime.now,
8748
+ });
8749
+ }
8750
+ catch (error) {
8751
+ const message = error instanceof Error ? error.message : String(error);
8752
+ if (message.includes('\nNext:')) {
8753
+ throw error;
8754
+ }
8755
+ throw new Error(`${message}\nNext: ${memoryRecoveryForArtifact(input.command.artifactId, input.command.dir)}`);
8756
+ }
8757
+ }
8758
+ async function runMemoryStart(command, io, runtime) {
8759
+ const appended = await appendCliMemoryEntry({
8760
+ command,
8761
+ kind: 'intent',
8762
+ summary: command.summary,
8763
+ body: command.body,
8764
+ tags: ['session'],
8765
+ runtime,
8766
+ });
8767
+ const result = {
8768
+ ok: true,
8769
+ command: command.command,
8770
+ ...formatMemoryAppendResult(appended),
8771
+ };
8772
+ if (command.json) {
8773
+ io.stdout(JSON.stringify(result, null, 2));
8774
+ }
8775
+ else {
8776
+ io.stdout([`memory: ${appended.entry.kind}`, `summary: ${appended.entry.summary}`, `session: ${appended.entry.sessionId}`].join('\n'));
8777
+ }
8778
+ return 0;
8779
+ }
8780
+ async function runMemoryAdd(command, io, runtime) {
8781
+ const appended = await appendCliMemoryEntry({
8782
+ command,
8783
+ kind: command.kind,
8784
+ summary: command.summary,
8785
+ body: command.body,
8786
+ tags: command.tags,
8787
+ runtime,
8788
+ });
8789
+ const result = {
8790
+ ok: true,
8791
+ command: command.command,
8792
+ ...formatMemoryAppendResult(appended),
8793
+ };
8794
+ if (command.json) {
8795
+ io.stdout(JSON.stringify(result, null, 2));
8796
+ }
8797
+ else {
8798
+ io.stdout([`memory: ${appended.entry.kind}`, `summary: ${appended.entry.summary}`].join('\n'));
8799
+ }
8800
+ return 0;
8801
+ }
8802
+ async function inferHandoffFromRecentEntries(input) {
8803
+ const entries = await readArtifactMemoryEntries({
8804
+ repoPath: input.repoPath,
8805
+ commitSha: input.commitSha,
8806
+ runner: commandRunnerForMemory(input.runtime),
8807
+ }).catch(() => []);
8808
+ const relevant = [...entries].reverse().find((entry) => entry.kind === 'failure' || entry.kind === 'open_question');
8809
+ return relevant?.summary ?? null;
8810
+ }
8811
+ async function runMemoryHandoff(command, io, runtime) {
8812
+ const repoPath = path.resolve(command.dir);
8813
+ const head = await readArtifactMemoryHead({
8814
+ repoPath,
8815
+ runner: commandRunnerForMemory(runtime),
8816
+ }).catch(() => null);
8817
+ const inferredReason = command.reason ?? (await inferHandoffFromRecentEntries({ repoPath, runtime, commitSha: head }));
8818
+ if (!inferredReason && !command.summary) {
8819
+ throw new TenderCliUsageError('memory handoff needs --reason or a recent failure/open_question entry to infer from.\nNext: tender memory handoff --reason "Need Tender support because ..." --summary "Current blocker and what was tried"');
8820
+ }
8821
+ const summary = command.summary ?? inferredReason ?? 'Tender support handoff';
8822
+ const body = [
8823
+ inferredReason ? `Reason: ${inferredReason}` : null,
8824
+ command.body,
8825
+ 'Include goal, current failure, commands tried, changed files, suspected causes, and what not to retry.',
8826
+ ]
8827
+ .filter((line) => Boolean(line && line.trim()))
8828
+ .join('\n');
8829
+ const appended = await appendCliMemoryEntry({
8830
+ command,
8831
+ kind: 'support_handoff',
8832
+ summary,
8833
+ body,
8834
+ tags: ['support', 'handoff'],
8835
+ status: 'open',
8836
+ severity: 'warning',
8837
+ runtime,
8838
+ });
8839
+ const result = {
8840
+ ok: true,
8841
+ command: command.command,
8842
+ reason: inferredReason,
8843
+ ...formatMemoryAppendResult(appended),
8844
+ };
8845
+ if (command.json) {
8846
+ io.stdout(JSON.stringify(result, null, 2));
8847
+ }
8848
+ else {
8849
+ io.stdout([`memory: support_handoff`, `summary: ${appended.entry.summary}`, `next: tender memory flush --dir ${command.dir}`].join('\n'));
8850
+ }
8851
+ return 0;
8852
+ }
8853
+ async function runMemoryShow(command, io, runtime) {
8854
+ const repoPath = path.resolve(command.dir);
8855
+ const entries = await readArtifactMemoryEntries({
8856
+ repoPath,
8857
+ commitSha: command.commitSha,
8858
+ runner: commandRunnerForMemory(runtime),
8859
+ });
8860
+ const result = {
8861
+ ok: true,
8862
+ command: command.command,
8863
+ ref: ARTIFACT_MEMORY_NOTES_REF,
8864
+ commitSha: command.commitSha ?? (await readArtifactMemoryHead({ repoPath, runner: commandRunnerForMemory(runtime) })),
8865
+ entries,
8866
+ };
8867
+ if (command.json) {
8868
+ io.stdout(JSON.stringify(result, null, 2));
8869
+ }
8870
+ else {
8871
+ io.stdout(entries.length > 0 ? entries.map(formatArtifactMemoryEntry).join('\n') : 'No artifact memory entries found for this commit.');
8872
+ }
8873
+ return 0;
8874
+ }
8875
+ async function runMemoryBrief(command, io, runtime) {
8876
+ const repoPath = path.resolve(command.dir);
8877
+ const entries = await readArtifactMemoryEntries({
8878
+ repoPath,
8879
+ runner: commandRunnerForMemory(runtime),
8880
+ });
8881
+ const latestSummary = [...entries].reverse().find((entry) => entry.kind === 'summary') ?? null;
8882
+ const unresolved = entries
8883
+ .filter((entry) => entry.status === 'open' || entry.kind === 'failure' || entry.kind === 'open_question' || entry.kind === 'support_handoff')
8884
+ .slice(-command.limit);
8885
+ const briefEntries = latestSummary ? [latestSummary, ...unresolved.filter((entry) => entry.entryId !== latestSummary.entryId)] : unresolved;
8886
+ const result = {
8887
+ ok: true,
8888
+ command: command.command,
8889
+ ref: ARTIFACT_MEMORY_NOTES_REF,
8890
+ entries: briefEntries,
8891
+ };
8892
+ if (command.json) {
8893
+ io.stdout(JSON.stringify(result, null, 2));
8894
+ }
8895
+ else {
8896
+ io.stdout(briefEntries.length > 0 ? briefEntries.map(formatArtifactMemoryEntry).join('\n') : 'No artifact memory brief is available yet.');
8897
+ }
8898
+ return 0;
8899
+ }
8900
+ async function runMemoryFlush(command, io, runtime) {
8901
+ const sync = await pushArtifactMemoryNotes({
8902
+ repoPath: path.resolve(command.dir),
8903
+ remoteName: command.remoteName,
8904
+ runner: commandRunnerForMemory(runtime),
8905
+ });
8906
+ const next = sync.status === 'failed'
8907
+ ? `tender memory flush --dir ${command.dir} --remote ${command.remoteName}`
8908
+ : sync.status === 'not_configured'
8909
+ ? memoryRecoveryForArtifact(command.artifactId, command.dir)
8910
+ : null;
8911
+ const result = {
8912
+ ok: sync.status === 'pushed',
8913
+ command: command.command,
8914
+ memorySync: sync,
8915
+ next,
8916
+ };
8917
+ if (command.json) {
8918
+ io.stdout(JSON.stringify(result, null, 2));
8919
+ }
8920
+ else {
8921
+ io.stdout([
8922
+ `memory_sync: ${sync.status}`,
8923
+ sync.error ? `error: ${sync.error}` : null,
8924
+ result.next ? `next: ${result.next}` : null,
8925
+ ]
8926
+ .filter((line) => Boolean(line))
8927
+ .join('\n'));
8928
+ }
8929
+ return sync.status === 'pushed' ? 0 : 1;
8930
+ }
8931
+ async function runMemoryExport(command, io, runtime) {
8932
+ const entries = await readArtifactMemoryEntries({
8933
+ repoPath: path.resolve(command.dir),
8934
+ commitSha: command.commitSha,
8935
+ runner: commandRunnerForMemory(runtime),
8936
+ });
8937
+ if (command.format === 'jsonl') {
8938
+ io.stdout(entries.map((entry) => JSON.stringify(entry)).join('\n'));
8939
+ return 0;
8940
+ }
8941
+ throw new TenderCliUsageError(`Unsupported memory export format: ${command.format}.\nNext: tender memory export --format jsonl --dir ${command.dir}`);
8942
+ }
8943
+ async function readLocalChangedFiles(input) {
8944
+ const result = await runGitCommand({
8945
+ runtime: input.runtime,
8946
+ cwd: input.repoPath,
8947
+ args: ['status', '--porcelain'],
8948
+ allowFailure: true,
8949
+ });
8950
+ if (result.exitCode !== 0) {
8951
+ return [];
8952
+ }
8953
+ return result.stdout
8954
+ .split(/\r?\n/)
8955
+ .map((line) => line.slice(3).trim())
8956
+ .filter(Boolean)
8957
+ .slice(0, 50);
8958
+ }
8959
+ async function appendAutomaticMemory(input) {
8960
+ try {
8961
+ return await appendArtifactMemoryNote({
8962
+ repoPath: input.repoPath,
8963
+ artifactId: input.artifactId,
8964
+ kind: input.kind,
8965
+ summary: input.summary,
8966
+ body: input.body,
8967
+ tags: input.tags,
8968
+ severity: input.severity,
8969
+ status: input.status,
8970
+ related: input.related,
8971
+ source: {
8972
+ actor: 'cli',
8973
+ command: input.summary,
8974
+ },
8975
+ runner: commandRunnerForMemory(input.runtime),
8976
+ now: input.runtime.now,
8977
+ });
8978
+ }
8979
+ catch (error) {
8980
+ return {
8981
+ error: error instanceof Error ? error.message : String(error),
8982
+ next: `tender memory add --dir ${input.repoPath} --artifact ${input.artifactId} --kind ${input.kind} --summary ${JSON.stringify(input.summary)}`,
8983
+ };
8984
+ }
8985
+ }
8986
+ async function flushAutomaticMemory(input) {
8987
+ return pushArtifactMemoryNotes({
8988
+ repoPath: input.repoPath,
8989
+ remoteName: input.remoteName,
8990
+ runner: commandRunnerForMemory(input.runtime),
8991
+ });
8992
+ }
8340
8993
  async function resolveAnalyticsCredentials(command, runtime) {
8341
8994
  return await resolveApiCredentials({
8342
8995
  baseUrl: command.baseUrl,
@@ -9026,6 +9679,30 @@ export async function runTenderCli(args, io = {
9026
9679
  else if (command.topic === 'auth status') {
9027
9680
  io.stdout(authStatusHelp());
9028
9681
  }
9682
+ else if (command.topic === 'memory') {
9683
+ io.stdout(memoryHelp());
9684
+ }
9685
+ else if (command.topic === 'memory start') {
9686
+ io.stdout(memoryHelp());
9687
+ }
9688
+ else if (command.topic === 'memory add') {
9689
+ io.stdout(memoryAddHelp());
9690
+ }
9691
+ else if (command.topic === 'memory handoff') {
9692
+ io.stdout(memoryHandoffHelp());
9693
+ }
9694
+ else if (command.topic === 'memory show') {
9695
+ io.stdout(memoryHelp());
9696
+ }
9697
+ else if (command.topic === 'memory brief') {
9698
+ io.stdout(memoryHelp());
9699
+ }
9700
+ else if (command.topic === 'memory flush') {
9701
+ io.stdout(memoryHelp());
9702
+ }
9703
+ else if (command.topic === 'memory export') {
9704
+ io.stdout(memoryHelp());
9705
+ }
9029
9706
  else if (command.topic === 'playbooks') {
9030
9707
  io.stdout(playbooksHelp());
9031
9708
  }
@@ -9208,6 +9885,27 @@ export async function runTenderCli(args, io = {
9208
9885
  if (command.command === 'auth status') {
9209
9886
  return await runAuthStatus(command, io, runtime);
9210
9887
  }
9888
+ if (command.command === 'memory start') {
9889
+ return await runMemoryStart(command, io, runtime);
9890
+ }
9891
+ if (command.command === 'memory add') {
9892
+ return await runMemoryAdd(command, io, runtime);
9893
+ }
9894
+ if (command.command === 'memory handoff') {
9895
+ return await runMemoryHandoff(command, io, runtime);
9896
+ }
9897
+ if (command.command === 'memory show') {
9898
+ return await runMemoryShow(command, io, runtime);
9899
+ }
9900
+ if (command.command === 'memory brief') {
9901
+ return await runMemoryBrief(command, io, runtime);
9902
+ }
9903
+ if (command.command === 'memory flush') {
9904
+ return await runMemoryFlush(command, io, runtime);
9905
+ }
9906
+ if (command.command === 'memory export') {
9907
+ return await runMemoryExport(command, io, runtime);
9908
+ }
9211
9909
  if (command.command === 'playbooks list') {
9212
9910
  return await runPlaybooksList(command, io, runtime);
9213
9911
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"