forgeos 0.1.0-alpha.20 → 0.1.0-alpha.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/AGENTS.md +1 -1
- package/CHANGELOG.md +29 -2
- package/README.md +1 -1
- package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
- package/docs/changelog.md +43 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/types.ts +3 -0
- package/src/forge/agent-memory/bridge.ts +69 -4
- package/src/forge/agent-memory/context-pack.ts +106 -8
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +191 -2
- package/src/forge/agent-memory/types.ts +4 -1
- package/src/forge/brownfield-import/index.ts +68 -3
- package/src/forge/brownfield-import/types.ts +1 -1
- package/src/forge/cli/commands.ts +47 -0
- package/src/forge/cli/main.ts +4 -0
- package/src/forge/cli/new.ts +3 -1
- package/src/forge/cli/parse.ts +64 -11
- package/src/forge/cli/studio.ts +54 -0
- package/src/forge/cli/verify.ts +2 -0
- package/src/forge/compiler/frontend-graph/build.ts +58 -2
- package/src/forge/delta/index.ts +12 -0
- package/src/forge/delta/recorder.ts +60 -0
- package/src/forge/delta/status.ts +639 -2
- package/src/forge/delta/store.ts +204 -5
- package/src/forge/delta/timeline.ts +75 -1
- package/src/forge/version.ts +1 -1
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +33 -0
- package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/nuxt-web/src/commands/createNote.ts +26 -0
- package/templates/nuxt-web/src/forge/schema.ts +12 -0
- package/templates/nuxt-web/src/policies.ts +6 -0
- package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
- package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
- package/templates/nuxt-web/tsconfig.json +17 -0
- package/templates/nuxt-web/web/app.vue +67 -0
- package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
- package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
- package/templates/nuxt-web/web/composables/forge.ts +13 -0
- package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
- package/templates/nuxt-web/web/nuxt.config.ts +11 -0
- package/templates/nuxt-web/web/package.json +17 -0
- package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
- package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
- package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
- package/templates/nuxt-web/web/tsconfig.json +3 -0
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.22 input=d8837238db9cf8868eceae3e3cc5a7d9bb46a0955c55f0143caa802597d5a3a5 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.0-alpha.22
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- Improve the post-alpha.21 agent workflow without adding new MCP tools.
|
|
10
|
+
|
|
11
|
+
- Add `forge agent context` scopes for entry, change, proof, and handoff context packs.
|
|
12
|
+
- Add DeltaDB verbose health details for queue redaction, operation age, semantic projection state, and overhead posture.
|
|
13
|
+
- Add `forge delta compact`, `forge delta prune`, and redacted `forge delta export` for local Delta maintenance and support bundles.
|
|
14
|
+
- Add `forge doctor delta` for recorder writability, queue drain, redaction, and gitignore checks.
|
|
15
|
+
- Add Semantic Timeline summary data for stale proofs and causal chains.
|
|
16
|
+
- Record CAIR snapshot/query/action activity as Delta timeline events without adding new MCP tools.
|
|
17
|
+
- Add a Studio snapshot handoff block and a dedicated CAIR Protocol documentation page.
|
|
18
|
+
- Add an official `nuxt-web` template with a Forge notes backend, client/server Nuxt plugins, a `useNotes` composable, a Nitro runtime-config route, and generated Vue composables.
|
|
19
|
+
|
|
20
|
+
## 0.1.0-alpha.21
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Harden Codex hook queue privacy and brownfield import classification.
|
|
25
|
+
|
|
26
|
+
- Queue new Codex hook events as redacted payloads instead of storing raw prompts, tool inputs, tool responses, or transcripts in `.forge/agent/events.ndjson`.
|
|
27
|
+
- Compact consumed hook queue history into redacted `.history` lines so old raw queue entries are not copied forward during drain retention.
|
|
28
|
+
- Scope brownfield route classification to the detected route handler, so read-only GET handlers are not marked command-like because a sibling route in the same file writes state.
|
|
29
|
+
- Mark read-shaped `POST /search`, `/query`, `/filter`, `/lookup`, and `/graphql` routes as `command-candidate` with `ambiguous-post-query` risk instead of treating them as normal writes.
|
|
30
|
+
- Sync the public docs changelog/CLI reference and clarify the alpha/latest npm dist-tag policy.
|
|
31
|
+
|
|
3
32
|
## 0.1.0-alpha.20
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
|
@@ -11,8 +40,6 @@
|
|
|
11
40
|
- Preserve empty stdio command arguments, diagnose malformed command strings, and support structured `service.commandArgs` in external manifests.
|
|
12
41
|
- Include the basic example client demo in typecheck coverage.
|
|
13
42
|
|
|
14
|
-
## Unreleased
|
|
15
|
-
|
|
16
43
|
## 0.1.0-alpha.19
|
|
17
44
|
|
|
18
45
|
Alpha hardening:
|
package/README.md
CHANGED
|
@@ -413,7 +413,7 @@ Configure npm Trusted Publisher for package `forgeos`:
|
|
|
413
413
|
| Environment | blank |
|
|
414
414
|
| Allowed action | `npm publish` |
|
|
415
415
|
|
|
416
|
-
Do not add `NPM_TOKEN` for normal
|
|
416
|
+
Do not add `NPM_TOKEN` for normal alpha publishes. Alpha releases publish with the `alpha` dist-tag through npm OIDC Trusted Publisher so prerelease builds do not become `latest` accidentally. Configure `NPM_TOKEN` only when maintainers intentionally want the workflow to promote `latest` with `npm dist-tag add forgeos@<version> latest`; otherwise that step is skipped and `latest` may lag behind `alpha` during hardening. Use `release:publish-local-alpha -- --dry-run` only to validate the staged tarball locally; real npm publishing should go through `release:publish-alpha`, which dispatches `publish.yml` and uses npm OIDC Trusted Publisher. The workflow checks whether the package version already exists before installing dependencies or running tests, then uses `id-token: write`, Node 24/npm 11+, and provenance for the actual publish. `npm run release:smoke` runs `npm pack`, creates a fresh app with the packed tarball, installs dependencies, runs `forge dev --once --json`, and verifies the app smoke path.
|
|
417
417
|
|
|
418
418
|
## Milestone History
|
|
419
419
|
|
|
Binary file
|
package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar
CHANGED
|
Binary file
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,49 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.22
|
|
10
|
+
|
|
11
|
+
- Added focused post-alpha.21 workflow improvements without expanding MCP tools:
|
|
12
|
+
scoped Agent Memory context packs, DeltaDB verbose health details, Semantic
|
|
13
|
+
Timeline stale-proof/causal summaries, Studio snapshot handoff metadata,
|
|
14
|
+
local Delta maintenance commands (`compact`, `prune`, redacted `export`),
|
|
15
|
+
`forge doctor delta`, CAIR timeline events, and a dedicated CAIR Protocol
|
|
16
|
+
documentation page.
|
|
17
|
+
- Added an official `nuxt-web` template: a Forge notes backend plus Nuxt app
|
|
18
|
+
using client/server Forge plugins, `web/composables/useNotes.ts`, generated
|
|
19
|
+
Vue composables, a Nitro runtime-config route, and
|
|
20
|
+
`NUXT_PUBLIC_FORGE_URL`.
|
|
21
|
+
|
|
22
|
+
## 0.1.0-alpha.21
|
|
23
|
+
|
|
24
|
+
Alpha.21 hardens external-agent privacy and brownfield import polish:
|
|
25
|
+
|
|
26
|
+
- Codex hook runner queue entries now store redacted payloads instead of raw
|
|
27
|
+
prompts, tool inputs, tool responses, or transcripts.
|
|
28
|
+
- Consumed hook queue history is compacted as redacted `.history` entries, so
|
|
29
|
+
old raw queue lines are not copied forward during retention.
|
|
30
|
+
- Brownfield import now scopes write/side-effect heuristics to the detected
|
|
31
|
+
route handler when possible, preventing sibling mutating routes from making a
|
|
32
|
+
read-only GET route look command-like.
|
|
33
|
+
- Read-shaped `POST /search`, `/query`, `/filter`, `/lookup`, and `/graphql`
|
|
34
|
+
routes are emitted as `command-candidate` with `ambiguous-post-query` risk
|
|
35
|
+
until a human review decides whether they should become Forge queries or
|
|
36
|
+
commands.
|
|
37
|
+
- CLI/reference docs now include the CAIR agent protocol and clarify the
|
|
38
|
+
`alpha`/`latest` npm dist-tag policy.
|
|
39
|
+
|
|
40
|
+
## 0.1.0-alpha.20
|
|
41
|
+
|
|
42
|
+
Generated-change and hook queue fixes:
|
|
43
|
+
|
|
44
|
+
- Fixed generated-change diagnostics for `AGENTS.md` generated blocks and
|
|
45
|
+
`.forge/agent/context.json`.
|
|
46
|
+
- Skipped probe, invalid, and out-of-workspace queued hook events during Agent
|
|
47
|
+
Memory drain, and bounded large hook queue inspection.
|
|
48
|
+
- Preserved empty stdio command arguments, diagnosed malformed command strings,
|
|
49
|
+
and supported structured `service.commandArgs` in external manifests.
|
|
50
|
+
- Included the basic example client demo in typecheck coverage.
|
|
51
|
+
|
|
9
52
|
## 0.1.0-alpha.19
|
|
10
53
|
|
|
11
54
|
Alpha hardening:
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.22","releaseId":"forgeos@0.1.0-alpha.22+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.22 input=d8837238db9cf8868eceae3e3cc5a7d9bb46a0955c55f0143caa802597d5a3a5 content=bb05ee3635ddeea2d84168f8bf728e07f82e896061e8494b17f56dea26000770
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.22",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.22+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
|
@@ -4,6 +4,7 @@ import { createDiagnostic } from "../compiler/diagnostics/create.ts";
|
|
|
4
4
|
import { createDeltaId } from "../delta/ids.ts";
|
|
5
5
|
import { DeltaStore, DeltaStoreBusyError, describeDeltaStoreBusy, summarizeDeltaStoreBusy } from "../delta/store.ts";
|
|
6
6
|
import { extractAgentEventBindings, normalizeAgentEvent, summarizeAgentEvent } from "./normalize.ts";
|
|
7
|
+
import { redactAgentPayload } from "./redaction.ts";
|
|
7
8
|
import { buildAgentMemoryContext } from "./context-pack.ts";
|
|
8
9
|
import { claudeCodeInstallFiles, claudeCodeInstallResult } from "./sources/claude-code.ts";
|
|
9
10
|
import { codexInstallFiles, codexInstallResult, privacyDefaults } from "./sources/codex.ts";
|
|
@@ -28,6 +29,9 @@ export interface AgentMemoryCommandOptions {
|
|
|
28
29
|
eventName?: string;
|
|
29
30
|
input?: unknown;
|
|
30
31
|
entry?: string;
|
|
32
|
+
change?: string;
|
|
33
|
+
proof?: string;
|
|
34
|
+
handoff?: boolean;
|
|
31
35
|
current?: boolean;
|
|
32
36
|
dryRun?: boolean;
|
|
33
37
|
force?: boolean;
|
|
@@ -307,6 +311,9 @@ export async function runAgentMemoryCommand(options: AgentMemoryCommandOptions):
|
|
|
307
311
|
return await buildAgentMemoryContext({
|
|
308
312
|
workspaceRoot: options.workspaceRoot,
|
|
309
313
|
entry: options.entry,
|
|
314
|
+
change: options.change,
|
|
315
|
+
proof: options.proof,
|
|
316
|
+
handoff: options.handoff,
|
|
310
317
|
limit: options.limit,
|
|
311
318
|
});
|
|
312
319
|
} catch (error) {
|
|
@@ -497,15 +504,62 @@ function compactAgentMemoryQueueFile(options: {
|
|
|
497
504
|
}
|
|
498
505
|
mkdirSync(dirname(historyFile), { recursive: true });
|
|
499
506
|
const existingHistory = existsSync(historyFile) ? readFileSync(historyFile) : Buffer.alloc(0);
|
|
507
|
+
const redactedConsumedHistory = redactedQueueHistoryBuffer(originalConsumed);
|
|
500
508
|
writeFileSync(
|
|
501
509
|
historyFile,
|
|
502
|
-
trimBufferStart(Buffer.concat([existingHistory,
|
|
510
|
+
trimBufferStart(Buffer.concat([existingHistory, redactedConsumedHistory]), options.historyMaxBytes),
|
|
503
511
|
);
|
|
504
512
|
writeFileSync(options.watchFile, currentBuffer.subarray(options.consumedOffset));
|
|
505
513
|
writeQueueCheckpoint(options.watchFile, 0);
|
|
506
514
|
return { compacted: true, historyFile };
|
|
507
515
|
}
|
|
508
516
|
|
|
517
|
+
function redactedQueueHistoryBuffer(consumedBuffer: Buffer): Buffer {
|
|
518
|
+
const { complete } = splitCompleteJsonLines(consumedBuffer);
|
|
519
|
+
const lines: string[] = [];
|
|
520
|
+
for (const line of complete) {
|
|
521
|
+
if (!line.raw.trim()) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const parsed = normalizeRawInput(line.raw);
|
|
525
|
+
if (!parsed) {
|
|
526
|
+
lines.push(JSON.stringify({
|
|
527
|
+
forgeHookQueueV1: true,
|
|
528
|
+
historyRedacted: true,
|
|
529
|
+
rawStored: false,
|
|
530
|
+
payloadRedacted: true,
|
|
531
|
+
payload: { _parseError: true },
|
|
532
|
+
}));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
lines.push(JSON.stringify(redactedQueueHistoryEntry(parsed)));
|
|
536
|
+
}
|
|
537
|
+
return Buffer.from(lines.length > 0 ? `${lines.join("\n")}\n` : "", "utf8");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function redactedQueueHistoryEntry(parsed: Record<string, unknown>): Record<string, unknown> {
|
|
541
|
+
if (parsed.forgeHookQueueV1 !== true) {
|
|
542
|
+
return {
|
|
543
|
+
historyRedacted: true,
|
|
544
|
+
rawStored: false,
|
|
545
|
+
payloadRedacted: true,
|
|
546
|
+
payload: redactAgentPayload(parsed).value,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const queuedPayload = objectField(parsed, "payload") ?? objectField(parsed, "raw") ?? {};
|
|
550
|
+
return {
|
|
551
|
+
forgeHookQueueV1: true,
|
|
552
|
+
source: typeof parsed.source === "string" ? parsed.source : "codex",
|
|
553
|
+
eventName: typeof parsed.eventName === "string" ? parsed.eventName : undefined,
|
|
554
|
+
workspaceRoot: typeof parsed.workspaceRoot === "string" ? parsed.workspaceRoot : undefined,
|
|
555
|
+
enqueuedAt: typeof parsed.enqueuedAt === "string" ? parsed.enqueuedAt : undefined,
|
|
556
|
+
historyRedacted: true,
|
|
557
|
+
rawStored: false,
|
|
558
|
+
payloadRedacted: true,
|
|
559
|
+
payload: parsed.payloadRedacted === true ? queuedPayload : redactAgentPayload(queuedPayload).value,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
509
563
|
function splitCompleteJsonLines(buffer: Buffer): {
|
|
510
564
|
complete: Array<{ raw: string; endOffset: number }>;
|
|
511
565
|
completeBytes: number;
|
|
@@ -1004,6 +1058,12 @@ function formatAgentMemoryContextHuman(result: AgentMemoryContextPack): string {
|
|
|
1004
1058
|
lines.push(` - ${question}`);
|
|
1005
1059
|
}
|
|
1006
1060
|
}
|
|
1061
|
+
if (result.recommendedCommands.length > 0) {
|
|
1062
|
+
lines.push("", "Next:");
|
|
1063
|
+
for (const command of result.recommendedCommands.slice(0, 6)) {
|
|
1064
|
+
lines.push(` ${command}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1007
1067
|
return `${lines.join("\n")}\n`;
|
|
1008
1068
|
}
|
|
1009
1069
|
|
|
@@ -1104,6 +1164,11 @@ function normalizeRawInput(input: unknown): Record<string, unknown> | null {
|
|
|
1104
1164
|
return null;
|
|
1105
1165
|
}
|
|
1106
1166
|
|
|
1167
|
+
function objectField(value: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
|
|
1168
|
+
const child = value[key];
|
|
1169
|
+
return child && typeof child === "object" && !Array.isArray(child) ? child as Record<string, unknown> : undefined;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1107
1172
|
export async function readStdinJson(options?: { timeoutMs?: number }): Promise<unknown> {
|
|
1108
1173
|
if (process.stdin.isTTY) {
|
|
1109
1174
|
return undefined;
|
|
@@ -1150,15 +1215,15 @@ function parseQueuedHookLine(raw: Record<string, unknown>): {
|
|
|
1150
1215
|
if (raw.forgeHookQueueV1 !== true) {
|
|
1151
1216
|
return null;
|
|
1152
1217
|
}
|
|
1153
|
-
const payload = raw
|
|
1154
|
-
if (!payload
|
|
1218
|
+
const payload = objectField(raw, "payload") ?? objectField(raw, "raw");
|
|
1219
|
+
if (!payload) {
|
|
1155
1220
|
return null;
|
|
1156
1221
|
}
|
|
1157
1222
|
return {
|
|
1158
1223
|
source: typeof raw.source === "string" ? raw.source : "codex",
|
|
1159
1224
|
eventName: typeof raw.eventName === "string" ? raw.eventName : undefined,
|
|
1160
1225
|
workspaceRoot: typeof raw.workspaceRoot === "string" ? raw.workspaceRoot : undefined,
|
|
1161
|
-
payload
|
|
1226
|
+
payload,
|
|
1162
1227
|
};
|
|
1163
1228
|
}
|
|
1164
1229
|
|
|
@@ -4,14 +4,26 @@ import type { AgentMemoryContextEvent, AgentMemoryContextPack, AgentMemoryEventR
|
|
|
4
4
|
export async function buildAgentMemoryContext(input: {
|
|
5
5
|
workspaceRoot: string;
|
|
6
6
|
entry?: string;
|
|
7
|
+
change?: string;
|
|
8
|
+
proof?: string;
|
|
9
|
+
handoff?: boolean;
|
|
7
10
|
limit?: number;
|
|
8
11
|
}): Promise<AgentMemoryContextPack> {
|
|
9
12
|
const store = await DeltaStore.open(input.workspaceRoot, { access: "read" });
|
|
10
13
|
try {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const scope = contextScope(input);
|
|
15
|
+
const target = contextTarget(input, scope);
|
|
16
|
+
const currentSession = await store.currentWorkSession();
|
|
17
|
+
const sessionId = scope === "change" && (input.change === "current" || !input.change)
|
|
18
|
+
? currentSession?.id
|
|
19
|
+
: undefined;
|
|
20
|
+
const events = await store.listAgentMemoryEvents({ target: eventTarget(input, scope), limit: input.limit ?? 50 });
|
|
21
|
+
const timeline = target || sessionId
|
|
22
|
+
? await store.semanticTimeline({ target, workSessionId: sessionId, limit: input.limit ?? 50 })
|
|
23
|
+
: undefined;
|
|
24
|
+
const current = timeline?.currentState && Object.keys(timeline.currentState).length > 0
|
|
25
|
+
? timeline.currentState
|
|
26
|
+
: await currentSessionState(store, currentSession);
|
|
15
27
|
const goals = events
|
|
16
28
|
.filter((event) => event.normalizedKind === "agent.prompt.submitted")
|
|
17
29
|
.map((event) => ({
|
|
@@ -35,9 +47,12 @@ export async function buildAgentMemoryContext(input: {
|
|
|
35
47
|
const openQuestions = timeline?.openQuestions ?? [];
|
|
36
48
|
return {
|
|
37
49
|
ok: true,
|
|
38
|
-
scope
|
|
39
|
-
entry:
|
|
50
|
+
scope,
|
|
51
|
+
entry: input.entry,
|
|
52
|
+
change: input.change,
|
|
53
|
+
proof: input.proof,
|
|
40
54
|
currentState: current,
|
|
55
|
+
recommendedCommands: recommendedCommands(scope, input, currentSession?.id),
|
|
41
56
|
agentMemory: {
|
|
42
57
|
summary: {
|
|
43
58
|
events: contextEventItems.length,
|
|
@@ -68,6 +83,84 @@ export async function buildAgentMemoryContext(input: {
|
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
function contextScope(input: {
|
|
87
|
+
entry?: string;
|
|
88
|
+
change?: string;
|
|
89
|
+
proof?: string;
|
|
90
|
+
handoff?: boolean;
|
|
91
|
+
}): AgentMemoryContextPack["scope"] {
|
|
92
|
+
if (input.handoff) {
|
|
93
|
+
return "handoff";
|
|
94
|
+
}
|
|
95
|
+
if (input.proof) {
|
|
96
|
+
return "proof";
|
|
97
|
+
}
|
|
98
|
+
if (input.change) {
|
|
99
|
+
return "change";
|
|
100
|
+
}
|
|
101
|
+
return input.entry ? "entry" : "current";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function contextTarget(
|
|
105
|
+
input: { entry?: string; change?: string; proof?: string },
|
|
106
|
+
scope: AgentMemoryContextPack["scope"],
|
|
107
|
+
): string | undefined {
|
|
108
|
+
if (scope === "entry") {
|
|
109
|
+
return input.entry;
|
|
110
|
+
}
|
|
111
|
+
if (scope === "proof") {
|
|
112
|
+
return input.proof?.includes(":") ? input.proof : `proof:${input.proof}`;
|
|
113
|
+
}
|
|
114
|
+
if (scope === "change" && input.change && input.change !== "current") {
|
|
115
|
+
return input.change.includes(":") ? input.change : `session:${input.change}`;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function eventTarget(
|
|
121
|
+
input: { entry?: string; change?: string; proof?: string },
|
|
122
|
+
scope: AgentMemoryContextPack["scope"],
|
|
123
|
+
): string | undefined {
|
|
124
|
+
if (scope === "entry") {
|
|
125
|
+
return input.entry;
|
|
126
|
+
}
|
|
127
|
+
if (scope === "proof") {
|
|
128
|
+
return input.proof;
|
|
129
|
+
}
|
|
130
|
+
if (scope === "change" && input.change !== "current") {
|
|
131
|
+
return input.change;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function recommendedCommands(
|
|
137
|
+
scope: AgentMemoryContextPack["scope"],
|
|
138
|
+
input: { entry?: string; change?: string; proof?: string },
|
|
139
|
+
currentSessionId: string | undefined,
|
|
140
|
+
): string[] {
|
|
141
|
+
const commands = [
|
|
142
|
+
"forge agent timeline --json",
|
|
143
|
+
"forge delta status --verbose --json",
|
|
144
|
+
];
|
|
145
|
+
if (scope === "entry" && input.entry) {
|
|
146
|
+
commands.push(`forge timeline ${input.entry} --json`);
|
|
147
|
+
commands.push(`forge explain ${input.entry} --json`);
|
|
148
|
+
}
|
|
149
|
+
if (scope === "proof" && input.proof) {
|
|
150
|
+
commands.push(`forge timeline proof:${input.proof.replace(/^proof:/u, "")} --json`);
|
|
151
|
+
}
|
|
152
|
+
if (scope === "change") {
|
|
153
|
+
commands.push(`forge timeline --session ${input.change === "current" || !input.change ? "current" : input.change} --json`);
|
|
154
|
+
commands.push("forge changed --json");
|
|
155
|
+
}
|
|
156
|
+
if (scope === "handoff") {
|
|
157
|
+
commands.push("forge handoff --json");
|
|
158
|
+
commands.push(`forge timeline --session ${currentSessionId ?? "current"} --json`);
|
|
159
|
+
commands.push("forge changed --json");
|
|
160
|
+
}
|
|
161
|
+
return [...new Set(commands)];
|
|
162
|
+
}
|
|
163
|
+
|
|
71
164
|
function contextEvents(events: AgentMemoryEventRecord[]): AgentMemoryContextEvent[] {
|
|
72
165
|
return events.map((event) => {
|
|
73
166
|
const eventBindings = bindings(event);
|
|
@@ -92,8 +185,8 @@ function contextEvents(events: AgentMemoryEventRecord[]): AgentMemoryContextEven
|
|
|
92
185
|
});
|
|
93
186
|
}
|
|
94
187
|
|
|
95
|
-
async function currentSessionState(store: DeltaStore): Promise<Record<string, unknown>> {
|
|
96
|
-
const session = await store.currentWorkSession();
|
|
188
|
+
async function currentSessionState(store: DeltaStore, currentSession?: Awaited<ReturnType<DeltaStore["currentWorkSession"]>>): Promise<Record<string, unknown>> {
|
|
189
|
+
const session = currentSession ?? await store.currentWorkSession();
|
|
97
190
|
return session
|
|
98
191
|
? {
|
|
99
192
|
sessionId: session.id,
|
|
@@ -101,6 +194,11 @@ async function currentSessionState(store: DeltaStore): Promise<Record<string, un
|
|
|
101
194
|
inferredIntent: session.inferredIntent,
|
|
102
195
|
confidence: session.confidence,
|
|
103
196
|
status: session.status,
|
|
197
|
+
reasons: session.reasons.slice(0, 5).map((reason) => ({
|
|
198
|
+
signal: reason.signal,
|
|
199
|
+
weight: reason.weight,
|
|
200
|
+
...(reason.value ? { value: reason.value } : {}),
|
|
201
|
+
})),
|
|
104
202
|
}
|
|
105
203
|
: {};
|
|
106
204
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Lightweight Codex hook runner — no Forge CLI, no DeltaDB.
|
|
4
|
-
* Reads stdin with a short timeout, enqueues to .forge/agent/events.ndjson, exits.
|
|
4
|
+
* Reads stdin with a short timeout, enqueues a redacted event to .forge/agent/events.ndjson, exits.
|
|
5
5
|
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
7
8
|
import { dirname, join, resolve } from "node:path";
|
|
8
9
|
|
|
@@ -17,6 +18,24 @@ if (!eventName) {
|
|
|
17
18
|
const workspaceRoot = resolve(process.cwd());
|
|
18
19
|
const eventsFile = join(workspaceRoot, ".forge", "agent", "events.ndjson");
|
|
19
20
|
|
|
21
|
+
const RAW_TEXT_KEYS = new Set([
|
|
22
|
+
"prompt",
|
|
23
|
+
"userPrompt",
|
|
24
|
+
"last_assistant_message",
|
|
25
|
+
"lastAssistantMessage",
|
|
26
|
+
"completion",
|
|
27
|
+
"message",
|
|
28
|
+
"transcript",
|
|
29
|
+
"transcript_path",
|
|
30
|
+
"transcriptPath",
|
|
31
|
+
"output",
|
|
32
|
+
"stdout",
|
|
33
|
+
"stderr",
|
|
34
|
+
"result",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const RAW_ARGS_KEYS = new Set(["args", "arguments", "tool_input", "toolInput", "tool_response", "toolResponse", "input"]);
|
|
38
|
+
|
|
20
39
|
function readStdin(timeoutMs) {
|
|
21
40
|
return new Promise((resolveRead) => {
|
|
22
41
|
if (process.stdin.isTTY) {
|
|
@@ -72,13 +91,183 @@ async function main() {
|
|
|
72
91
|
eventName,
|
|
73
92
|
workspaceRoot,
|
|
74
93
|
enqueuedAt: new Date().toISOString(),
|
|
75
|
-
|
|
94
|
+
rawStored: false,
|
|
95
|
+
payloadRedacted: true,
|
|
96
|
+
payload: sanitizePayload(raw, eventName),
|
|
76
97
|
};
|
|
77
98
|
|
|
78
99
|
mkdirSync(dirname(eventsFile), { recursive: true });
|
|
79
100
|
appendFileSync(eventsFile, `${JSON.stringify(entry)}\n`, "utf8");
|
|
80
101
|
}
|
|
81
102
|
|
|
103
|
+
function sanitizePayload(raw, hookEventName) {
|
|
104
|
+
const payload = stripRawPayload(raw);
|
|
105
|
+
if (!payload.hook_event_name) {
|
|
106
|
+
payload.hook_event_name = hookEventName;
|
|
107
|
+
}
|
|
108
|
+
if (!payload.cwd) {
|
|
109
|
+
payload.cwd = workspaceRoot;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const toolInput = objectField(raw, "tool_input") ?? objectField(raw, "toolInput");
|
|
113
|
+
const toolResponse = objectField(raw, "tool_response") ?? objectField(raw, "toolResponse");
|
|
114
|
+
const command = stringField(toolInput, "command") ?? stringField(raw, "command");
|
|
115
|
+
if (command) {
|
|
116
|
+
payload.commandHash = hashStable(command);
|
|
117
|
+
payload.commandStored = false;
|
|
118
|
+
payload.commandSummary = summarizeCommand(command);
|
|
119
|
+
payload.commandKind = classifyCommand(stringField(raw, "tool_name") ?? stringField(raw, "toolName"), command);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const description = stringField(toolInput, "description");
|
|
123
|
+
if (description) {
|
|
124
|
+
payload.approvalDescriptionSummary = safeSummary(description, 180);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const exitCode = numberField(toolResponse, "exitCode") ?? numberField(toolResponse, "exit_code") ??
|
|
128
|
+
numberField(raw, "exitCode") ?? numberField(raw, "exit_code");
|
|
129
|
+
if (exitCode !== undefined) {
|
|
130
|
+
payload.exitCode = exitCode;
|
|
131
|
+
payload.resultStatus = exitCode === 0 ? "success" : "failed";
|
|
132
|
+
} else {
|
|
133
|
+
const status = stringField(toolResponse, "status") ?? stringField(raw, "status");
|
|
134
|
+
if (status) {
|
|
135
|
+
payload.resultStatus = status;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const responseSummary = summarizeToolResponse(toolResponse);
|
|
140
|
+
if (responseSummary) {
|
|
141
|
+
payload.responseSummary = responseSummary;
|
|
142
|
+
}
|
|
143
|
+
if (toolResponse) {
|
|
144
|
+
payload.responseHash = hashStable(JSON.stringify(toolResponse));
|
|
145
|
+
payload.responseStored = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return payload;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function stripRawPayload(value) {
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return value.slice(0, 50).map((item) => stripRawPayload(item));
|
|
154
|
+
}
|
|
155
|
+
if (!value || typeof value !== "object") {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
const output = {};
|
|
159
|
+
for (const [key, child] of Object.entries(value)) {
|
|
160
|
+
if (RAW_TEXT_KEYS.has(key)) {
|
|
161
|
+
output[`${key}Hash`] = hashStable(typeof child === "string" ? child : JSON.stringify(child ?? null));
|
|
162
|
+
output[`${key}Stored`] = false;
|
|
163
|
+
if (typeof child === "string" && !isPromptLikeKey(key)) {
|
|
164
|
+
const summary = safeSummary(child, 160);
|
|
165
|
+
if (summary) {
|
|
166
|
+
output[`${key}Summary`] = summary;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (RAW_ARGS_KEYS.has(key)) {
|
|
172
|
+
output[`${key}Hash`] = hashStable(JSON.stringify(child ?? null));
|
|
173
|
+
output[`${key}Stored`] = false;
|
|
174
|
+
output[`${key}Shape`] = describeShape(child);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
output[key] = stripRawPayload(child);
|
|
178
|
+
}
|
|
179
|
+
return output;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function objectField(value, key) {
|
|
183
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const child = value[key];
|
|
187
|
+
return child && typeof child === "object" && !Array.isArray(child) ? child : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stringField(value, key) {
|
|
191
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const child = value[key];
|
|
195
|
+
return typeof child === "string" && child.length > 0 ? child : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function numberField(value, key) {
|
|
199
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const child = value[key];
|
|
203
|
+
return typeof child === "number" && Number.isFinite(child) ? child : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function describeShape(value) {
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
return { kind: "array", length: value.length };
|
|
209
|
+
}
|
|
210
|
+
if (value && typeof value === "object") {
|
|
211
|
+
return {
|
|
212
|
+
kind: "object",
|
|
213
|
+
keys: Object.keys(value).slice(0, 20).sort(),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return { kind: typeof value };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function summarizeCommand(command) {
|
|
220
|
+
return safeSummary(
|
|
221
|
+
command
|
|
222
|
+
.replace(/--(token|api-key|apikey|password|secret)\s+[^\s]+/giu, "--$1 [REDACTED]")
|
|
223
|
+
.replace(/(["']?)(token|apiKey|api_key|password|secret)(["']?)\s*:\s*(["'])(.*?)\4/giu, "$1$2$3: \"[REDACTED]\""),
|
|
224
|
+
220,
|
|
225
|
+
) ?? "[command redacted]";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function summarizeToolResponse(response) {
|
|
229
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const text = stringField(response, "stdout") ?? stringField(response, "stderr") ??
|
|
233
|
+
stringField(response, "output") ?? stringField(response, "result");
|
|
234
|
+
return text ? safeSummary(text, 180) : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function safeSummary(value, maxLength) {
|
|
238
|
+
const normalized = scrubSecretTokens(value).replace(/\s+/gu, " ").trim();
|
|
239
|
+
if (!normalized) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 3)}...` : normalized;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function classifyCommand(toolName, command) {
|
|
246
|
+
if (toolName === "apply_patch" || command.includes("*** Begin Patch")) {
|
|
247
|
+
return "patch";
|
|
248
|
+
}
|
|
249
|
+
if (/^\s*(?:node|npm|bun|pnpm|yarn|forge|git)\b/u.test(command)) {
|
|
250
|
+
return "shell";
|
|
251
|
+
}
|
|
252
|
+
return "unknown";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isPromptLikeKey(key) {
|
|
256
|
+
return key.toLowerCase().includes("prompt") || key.toLowerCase().includes("completion") || key.toLowerCase().includes("message");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scrubSecretTokens(value) {
|
|
260
|
+
return value
|
|
261
|
+
.replace(/\bsk[-_][A-Za-z0-9_\-.]{8,}\b/gu, "[REDACTED]")
|
|
262
|
+
.replace(/\bnpm_[A-Za-z0-9]{16,}\b/gu, "[REDACTED]")
|
|
263
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9_]{16,}\b/gu, "[REDACTED]")
|
|
264
|
+
.replace(/\b(?:xox[baprs]-)[A-Za-z0-9-]{16,}\b/gu, "[REDACTED]");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function hashStable(value) {
|
|
268
|
+
return createHash("sha256").update(value).digest("hex");
|
|
269
|
+
}
|
|
270
|
+
|
|
82
271
|
main()
|
|
83
272
|
.then(() => process.exit(0))
|
|
84
273
|
.catch(() => process.exit(1));
|