@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.
- package/dist/artifact-memory.d.ts +103 -0
- package/dist/artifact-memory.js +283 -0
- package/dist/index.js +742 -5
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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(
|
|
8125
|
+
input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
|
|
7718
8126
|
}
|
|
7719
8127
|
else {
|
|
7720
|
-
input.io.stdout([
|
|
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
|
}
|