@tenderprompt/cli 0.1.20 → 0.1.22

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
@@ -5,7 +5,8 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { generateTenderAppEnvTypes, validateTenderApp, } from '@tenderprompt/tender-app-validator';
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';
@@ -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' };
@@ -5865,6 +6148,30 @@ function readPreviewUrlFromJob(job) {
5865
6148
  function readPublishedUrlFromJob(job) {
5866
6149
  return readObjectString(job?.result, 'publishedUrl');
5867
6150
  }
6151
+ function readReleaseGitRefFromResult(result) {
6152
+ const releaseGitRef = readObject(readObject(result)?.releaseGitRef);
6153
+ if (releaseGitRef?.type !== 'tag') {
6154
+ return null;
6155
+ }
6156
+ const tagName = readObjectString(releaseGitRef, 'name');
6157
+ const commitSha = readObjectString(releaseGitRef, 'commitSha');
6158
+ if (!tagName && !commitSha) {
6159
+ return null;
6160
+ }
6161
+ return {
6162
+ tagName,
6163
+ commitSha,
6164
+ };
6165
+ }
6166
+ function formatPublishResultLines(result) {
6167
+ const record = readObject(result);
6168
+ const releaseGitRef = readReleaseGitRefFromResult(result);
6169
+ return [
6170
+ ...(readObjectString(record, 'publishedUrl') ? [`published_url: ${readObjectString(record, 'publishedUrl')}`] : []),
6171
+ ...(releaseGitRef?.tagName ? [`release_tag: ${releaseGitRef.tagName}`] : []),
6172
+ ...(releaseGitRef?.commitSha ? [`source_commit: ${releaseGitRef.commitSha}`] : []),
6173
+ ];
6174
+ }
5868
6175
  function normalizeContextFilePath(filePath) {
5869
6176
  const normalized = path.posix.normalize(filePath.replaceAll('\\', '/')).replace(/^\/+/, '');
5870
6177
  if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized.includes('/../')) {
@@ -5872,6 +6179,9 @@ function normalizeContextFilePath(filePath) {
5872
6179
  }
5873
6180
  return normalized;
5874
6181
  }
6182
+ function isForbiddenSourceExportPath(filePath) {
6183
+ return isForbiddenTenderAppRepoPath(filePath) || isForbiddenTenderAppSecretPath(filePath);
6184
+ }
5875
6185
  function resolveContextOutputPath(root, relativePath) {
5876
6186
  const absolutePath = path.resolve(root, relativePath);
5877
6187
  const relativeFromRoot = path.relative(root, absolutePath);
@@ -5971,6 +6281,9 @@ async function collectArtifactWorkspaceSourceFiles(input) {
5971
6281
  if (!normalizedPath) {
5972
6282
  continue;
5973
6283
  }
6284
+ if (isForbiddenSourceExportPath(normalizedPath)) {
6285
+ continue;
6286
+ }
5974
6287
  if (entry.type === 'directory') {
5975
6288
  queue.push(entry.path);
5976
6289
  continue;
@@ -5996,7 +6309,11 @@ async function collectArtifactWorkspaceSourceFiles(input) {
5996
6309
  async function planSourceFiles(input) {
5997
6310
  const planned = [];
5998
6311
  for (const file of input.files) {
5999
- const absolutePath = resolveContextOutputPath(input.root, file.path);
6312
+ const relativePath = normalizeContextFilePath(file.path);
6313
+ if (!relativePath || isForbiddenSourceExportPath(relativePath)) {
6314
+ continue;
6315
+ }
6316
+ const absolutePath = resolveContextOutputPath(input.root, relativePath);
6000
6317
  if (!absolutePath) {
6001
6318
  throw new Error(`Source file path escapes the target directory: ${file.path}`);
6002
6319
  }
@@ -6012,7 +6329,7 @@ async function planSourceFiles(input) {
6012
6329
  action = input.force ? 'overwrite' : 'conflict';
6013
6330
  }
6014
6331
  planned.push({
6015
- path: file.path,
6332
+ path: relativePath,
6016
6333
  absolutePath,
6017
6334
  action,
6018
6335
  content: file.content,
@@ -7178,6 +7495,13 @@ async function runGitSetup(command, io, runtime) {
7178
7495
  credentialHelper: helper,
7179
7496
  });
7180
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
+ });
7181
7505
  const result = {
7182
7506
  ok: true,
7183
7507
  command: command.command,
@@ -7192,6 +7516,7 @@ async function runGitSetup(command, io, runtime) {
7192
7516
  authSource: credentials.source,
7193
7517
  profile: credentials.profileName,
7194
7518
  credentialHelper: helper,
7519
+ memoryFetch,
7195
7520
  commands: plannedCommands.map((parts) => parts.join(' ')),
7196
7521
  next: `git push ${command.remoteName} ${git.defaultBranch}`,
7197
7522
  };
@@ -7203,6 +7528,7 @@ async function runGitSetup(command, io, runtime) {
7203
7528
  `remote: ${command.remoteName}`,
7204
7529
  `remote_url: ${git.remoteUrl}`,
7205
7530
  `default_branch: ${git.defaultBranch}`,
7531
+ `memory_fetch: ${memoryFetch.status}`,
7206
7532
  `next: ${result.next}`,
7207
7533
  ].join('\n'));
7208
7534
  }
@@ -7334,6 +7660,14 @@ async function runArtifactsInit(command, io, runtime) {
7334
7660
  job: null,
7335
7661
  result: null,
7336
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;
7337
7671
  let seedPaths = [];
7338
7672
  let plannedFiles = await planContextFiles({
7339
7673
  root,
@@ -7362,6 +7696,11 @@ async function runArtifactsInit(command, io, runtime) {
7362
7696
  remoteUrl: git.remoteUrl,
7363
7697
  credentialHelper: helper,
7364
7698
  });
7699
+ memoryFetch = await fetchArtifactMemoryNotes({
7700
+ repoPath: root,
7701
+ remoteName: command.remoteName,
7702
+ runner: commandRunnerForMemory(runtime),
7703
+ });
7365
7704
  sourceCheckout = await maybeCheckoutInitialArtifactSource({
7366
7705
  runtime,
7367
7706
  root,
@@ -7603,6 +7942,22 @@ async function runArtifactsInit(command, io, runtime) {
7603
7942
  };
7604
7943
  }
7605
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 });
7606
7961
  }
7607
7962
  const result = {
7608
7963
  ok: true,
@@ -7623,6 +7978,9 @@ async function runArtifactsInit(command, io, runtime) {
7623
7978
  sourceExport,
7624
7979
  seedCommit,
7625
7980
  preview,
7981
+ memoryFetch,
7982
+ memory,
7983
+ memorySync,
7626
7984
  summary: {
7627
7985
  create: plannedFiles.filter((file) => file.action === 'create').length,
7628
7986
  overwrite: plannedFiles.filter((file) => file.action === 'overwrite').length,
@@ -7653,6 +8011,8 @@ async function runArtifactsInit(command, io, runtime) {
7653
8011
  `dir: ${root}`,
7654
8012
  `remote: ${command.remoteName}`,
7655
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,
7656
8016
  `next: ${result.next}`,
7657
8017
  ]
7658
8018
  .filter((line) => Boolean(line))
@@ -7695,6 +8055,26 @@ async function postLifecycle(input) {
7695
8055
  io: input.io,
7696
8056
  json: input.command.json,
7697
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
+ }
7698
8078
  return 0;
7699
8079
  }
7700
8080
  const payload = await parseJsonResponse(response);
@@ -7713,11 +8093,46 @@ async function postLifecycle(input) {
7713
8093
  job: payload.job ?? null,
7714
8094
  result: payload.result ?? null,
7715
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
+ };
7716
8124
  if (input.command.json) {
7717
- input.io.stdout(JSON.stringify(result, null, 2));
8125
+ input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
7718
8126
  }
7719
8127
  else {
7720
- input.io.stdout([`app_id: ${input.command.artifactId}`, `operation: ${result.operation}`, 'status: completed'].join('\n'));
8128
+ input.io.stdout([
8129
+ `app_id: ${input.command.artifactId}`,
8130
+ `operation: ${result.operation}`,
8131
+ 'status: completed',
8132
+ ...(input.operation === 'publish' ? formatPublishResultLines(result.result) : []),
8133
+ memorySync ? `memory_sync: ${memorySync.status}` : null,
8134
+ memorySync?.status === 'failed' ? `next: tender memory flush --dir .` : null,
8135
+ ].join('\n'));
7721
8136
  }
7722
8137
  return 0;
7723
8138
  }
@@ -8298,6 +8713,283 @@ async function runGitRemote(command, io, runtime) {
8298
8713
  }
8299
8714
  return 0;
8300
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
+ }
8301
8993
  async function resolveAnalyticsCredentials(command, runtime) {
8302
8994
  return await resolveApiCredentials({
8303
8995
  baseUrl: command.baseUrl,
@@ -8987,6 +9679,30 @@ export async function runTenderCli(args, io = {
8987
9679
  else if (command.topic === 'auth status') {
8988
9680
  io.stdout(authStatusHelp());
8989
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
+ }
8990
9706
  else if (command.topic === 'playbooks') {
8991
9707
  io.stdout(playbooksHelp());
8992
9708
  }
@@ -9169,6 +9885,27 @@ export async function runTenderCli(args, io = {
9169
9885
  if (command.command === 'auth status') {
9170
9886
  return await runAuthStatus(command, io, runtime);
9171
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
+ }
9172
9909
  if (command.command === 'playbooks list') {
9173
9910
  return await runPlaybooksList(command, io, runtime);
9174
9911
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"