Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agent
2 changes: 1 addition & 1 deletion .github/workflows/offline-package-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
node: ["22", "24"]
env:
SOURCE_HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
EXPECTED_GITLINK: 987c74f24598af35d6d77dd4d50073732cb8cd6a
EXPECTED_GITLINK: 06be413220dcf4e14f5a951d129536534688c3ec
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/registry-upgrade-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
env:
BASELINE_VERSION: ${{ matrix.baseline }}
SOURCE_HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
EXPECTED_GITLINK: 987c74f24598af35d6d77dd4d50073732cb8cd6a
EXPECTED_GITLINK: 06be413220dcf4e14f5a951d129536534688c3ec
EXPECTED_CANDIDATE_VERSION: "1.15.7"
DISPATCH_HEAD: ${{ inputs.expected_head }}
DISPATCH_VERSION: ${{ inputs.expected_version }}
Expand Down
120 changes: 116 additions & 4 deletions scripts/check-runtime-artifact-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

// ─── Check Runtime Artifact Location ──────────────────────────────────────────
// Mechanical gate for the L1 rule `runtime-artifacts-location.md`:
// in a cortex-agent-managed project, every runtime artifact (logs, probe
// scripts, per-run output, observations, scratch JSON) belongs under the
// project `.agent/` directory — never in /tmp, the home directory, or the
// project root.
// classify scripts, logs, and outputs by lifetime. Cross-command material
// belongs under the project `.agent/` directory; process plumbing may use the
// OS temp directory only when it is removed inside the same top-level command.
//
// The rule is written as prose, so prose alone does not keep a skill from
// shipping a `os.tmpdir()` draft directory to every managed project. This
Expand Down Expand Up @@ -98,6 +97,20 @@ const ARTIFACT_EXTENSIONS = Object.freeze([
"md", "json", "jsonl", "ndjson", "log", "txt", "csv", "yaml", "yml",
]);

// A host patch creates a file in one tool call and normally consumes it in a
// later call. That is already cross-command material, so every temp target is
// a violation even when the file is not named like a report or log.
const APPLY_PATCH_TARGET = /\*\*\*\s+(?:Add|Update|Delete) File:\s*([^\r\n]+)/gi;

// Shell/PowerShell writes do not go through Node's fs API. Detect the common
// operators explicitly; the destination, rather than any mention of a temp
// path, determines whether the line writes into a temp directory.
const SHELL_REDIRECT = /(?<![=!<>-])>{1,2}\s*(?:"([^"]+)"|'([^']+)'|([^\s;|&]+))/g;
const SHELL_TEMP_WRITER = /\b(?:tee|Out-File|Set-Content|Add-Content)\b[^\r\n]*/i;
const SHELL_OUTPUT_FLAG = /(?:--output(?:=|\s+)|(?:^|\s)-o\s+)(?:"([^"]+)"|'([^']+)'|([^\s;|&]+))/gi;
const SHELL_COPY = /\b(?:cp|mv|install|Copy-Item|Move-Item)\b([^\r\n]*)/i;
const HAS_SHELL_CLEANUP = /\btrap\s+[^\n]*(?:\brm\b|Remove-Item)|\brm\s+-[A-Za-z]*r[^\n]*\b(?:\$TMPDIR|\/tmp|\/private\/tmp)\/|\bRemove-Item\b[^\n]*(?:-Recurse|-Force)/i;

// Splits a file name into its stem tokens, so that "catalog.json" does not
// read as an artifact just because "log" appears inside it, while
// "session-signals.json" does.
Expand Down Expand Up @@ -145,6 +158,9 @@ const ALLOW_PATTERNS = Object.freeze([
/GIT_INDEX_FILE/,
// A fixture that is removed again in the same breath.
/rmSync|rimraf|removeSync|unlinkSync/,
// Promoting a temp file into .agent/ is the required repair, not a write
// back into temp.
/\bmv\b[^\n]*\/(?:tmp|private\/tmp|var\/folders)\/[^\n]*\.agent\//i,
]);

// A test file is where a fixture legitimately lives in a temp directory: the
Expand Down Expand Up @@ -176,6 +192,72 @@ function containsArtifactName(text) {
return namesArtifactFile(text) || ARTIFACT_DIRNAME.test(text);
}

function referencesTemp(text, tempVars) {
return containsTempSource(text) ||
[...tempVars].some((name) => new RegExp(`\\b${name}\\b`).test(text));
}

// Split the small command-line subset relevant to copy/move destinations.
// This is deliberately not a shell parser; it only keeps quoted paths intact
// so a destination containing spaces is still classified correctly.
function shellArguments(text) {
const args = [];
let current = "";
let quote = null;
for (let i = 0; i < text.length; i += 1) {
const ch = text[i];
if (quote) {
if (ch === quote) quote = null;
else current += ch;
continue;
}
if (ch === '"' || ch === "'") {
quote = ch;
continue;
}
if (/\s/.test(ch)) {
if (current) args.push(current);
current = "";
continue;
}
current += ch;
}
if (current) args.push(current);
return args;
}

// Return concrete destinations that are written by shell syntax on this line.
// A `mv /tmp/file .agent/...` source alone is not a write back into temp.
function shellTempDestinations(line, tempVars) {
// Markdown blockquotes begin with `>` and are documentation, not shell
// redirection. A shell command that writes without any command part is not
// useful in the repository patterns this gate needs to protect.
if (/^\s*>\s/.test(line)) return [];
const destinations = [];
let match;

for (const pattern of [SHELL_REDIRECT, SHELL_OUTPUT_FLAG]) {
pattern.lastIndex = 0;
while ((match = pattern.exec(line)) !== null) {
const target = match[1] || match[2] || match[3];
if (target && referencesTemp(target, tempVars)) destinations.push(target);
}
}

if (SHELL_TEMP_WRITER.test(line) && referencesTemp(line, tempVars)) {
destinations.push(line.trim().slice(0, 120));
}

const copy = line.match(SHELL_COPY);
if (copy) {
const args = shellArguments(copy[1]).filter((arg) => !arg.startsWith("-"));
const destination = args[args.length - 1];
if (destination && referencesTemp(destination, tempVars)) destinations.push(destination);
}

return destinations;
}

// ─── Taint analysis ───────────────────────────────────────────────────────────

// Extracts the first argument of a call, respecting nesting so that
Expand Down Expand Up @@ -250,11 +332,41 @@ function analyseFile(filePath, root) {
// in another is still caught.
const tempVars = new Set();
const artifactVars = new Set();
const hasShellCleanup = HAS_SHELL_CLEANUP.test(text);

const lines = text.split("\n");
lines.forEach((line, idx) => {
if (isAllowlisted(line)) return;

// apply_patch is a host edit: the file survives the tool call by design,
// so even a one-off script target crosses the process-local boundary.
APPLY_PATCH_TARGET.lastIndex = 0;
let patchTarget;
while ((patchTarget = APPLY_PATCH_TARGET.exec(line)) !== null) {
if (containsTempSource(patchTarget[1])) {
findings.push({
rule: "apply-patch-target-in-temp",
title: "host patch targets a temp path",
file: rel,
line: idx + 1,
detail: patchTarget[0].trim().slice(0, 120),
});
}
}

if (!hasShellCleanup) {
const destinations = shellTempDestinations(line, tempVars);
if (destinations.length > 0) {
findings.push({
rule: "shell-write-to-temp",
title: "shell syntax writes to a temp path",
file: rel,
line: idx + 1,
detail: destinations[0].slice(0, 120),
});
}
}

const assignment = line.match(ASSIGNMENT);
if (assignment) {
const [, name, expr] = assignment;
Expand Down
108 changes: 92 additions & 16 deletions templates/_shared/.agent/rules/runtime-artifacts-location.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
---
title: "Runtime Artifact Location"
description: '> All runtime artifacts produced by a cortex-agent-managed project — logs, probe scripts, per-run output, observations, scratch JSON — must live under the project .agent/ directory, never in /tmp, the home directory, or the project root.'
description: 'Classify every script, log, and output by lifetime. Cross-command, review, or handoff artifacts belong under .agent/; process-local scratch may use the OS temp directory only when it is deleted and verified within the same top-level command.'
type: rule
scope: L1
applicable_to:
- all
linked_workflows: []
linked_skills: []
owner: Kucell
last_verified: 2026-10-28
last_verified: 2026-11-07
status: stable
linked_decisions: ["D-OKF-001"]
---

# Runtime Artifact Location

> **Core rule**: in a project managed by cortex-agent, everything the agent produces while
> running, debugging, or investigating — logs, probe scripts, per-run output,
> observations, scratch JSON, screenshots — belongs under the project .agent/
> directory. It does **not** belong in /tmp, the home directory, or the project root.
> **Core rule**: choose a script, log, or output's location by its lifetime. If it
> must survive the producing process, or will be read by another command, reviewer,
> handoff, or task, it belongs under the project's `.agent/` directory. The OS temp
> directory is reserved for process plumbing that is deleted and verified within the
> same top-level command.
>
> **Cross-command means durable**: creating a script with `apply_patch`, recording its
> path for a later command, or letting another agent read its output crosses that
> boundary. Such files must not live in `/tmp`, `os.tmpdir()`, `%TEMP%`, or `$TMPDIR`.

Three questions determine the answer; the filename alone does not:

1. Must it survive after the current process or top-level command ends?
2. Will another command, reviewer, agent, or resumed task read it?
3. Is it a reusable probe, measurement, or delivery asset?

If any answer is yes, use the `.agent/` lifecycle table below.

---

## Lifetime and destination

| Lifetime / use | Location | Closeout requirement |
|---|---|---|
| Valid only while one process or top-level command runs | OS temp directory | Delete and verify absence in the same command |
| Needed across commands or for short review, but not durable evidence | `.agent/debug/temp/<task>-<run-id>/` | Keep a file inventory and retention condition |
| Reusable across tasks | `.agent/skills/<skill>/scripts/` | Provide an entry point and tests |
| Mission-specific tool or long-lived evidence | `.agent/missions/<id>/scripts/` or `evidence/` | Bind to source identity and verification result |
| Machine-readable summary for another agent | `.agent/artifacts/` | Keep compact references, hashes, and payloads |

Do not evade the rule by renaming a directory to `temp`, `scratch`, or `debug`.
Lifetime and consumer determine placement.

---

Expand Down Expand Up @@ -107,19 +135,51 @@ Use <run-id-or-sample-id>.<ext>:
- ❌ **Do not treat .agent/debug/ as release evidence.** Long-lived evidence
belongs in .agent/missions/<id>/evidence/ or the project documentation assets.

### The one legitimate exception
### Cross-command boundary

A top-level command is one indivisible host invocation, including synchronous child
processes in its process tree. The boundary has been crossed when:

- `apply_patch`, `Write`, or `Edit` creates a file and a later tool command uses it;
- command A creates a script or output and command B reads, uploads, forwards, or
updates it by absolute path;
- shell redirection, `tee`, `cp`, `mv`, or a program argument writes to a temp path
but cleanup happens in another command;
- a temp path is persisted in task progress, a handoff, README, configuration, or
another durable document.

Cross-command material must be created under `.agent/` directly. For a probe or output
needed briefly for review, use `.agent/debug/temp/<task>-<run-id>/` and record each
file's purpose and retention condition in `inventory.json`.

Short-lived **process plumbing** may use the OS temp directory, provided it is
genuinely ephemeral and holds nothing that must survive the process:
### Single-command process plumbing

- a private context file passed to a child process and deleted right after;
- a scratch build directory for a throwaway compile;
- a test fixture directory created and removed inside a single test case.
Process plumbing may use the OS temp directory only when all of these hold:

The test is simple: **if the artifact has value after the process exits, it
belongs under .agent/.** A secret-bearing context file also belongs nowhere
persistent — it must be mode 0600 and removed on completion, which is why it is
not a runtime artifact but process plumbing.
- its lifetime is bounded by a provable cleanup path in the same top-level command;
- `trap`, `finally`, or an equivalent mechanism cleans up on success, failure, and
interruption;
- absence is verified before the command returns;
- the path is not written into durable documentation, configuration, or a variable
table consumed by another command.

Examples are a child-process context file (mode `0600`), a throwaway compile directory,
and a fixture created and removed inside one test case. A PowerShell, Python, Node, or
shell probe reused across commands is not process plumbing.

### Task closeout

Before a task is complete, inventory every temp path created by the run and classify it
as one of:

- `deleted`: removed and verified absent;
- `promoted`: moved under `.agent/debug/temp/` or a longer-lived destination and all
references updated;
- `owned_by_live_process`: still used by a named running process, with the process and
retention condition recorded.

Any unclassified path makes the closeout `TEMP_ARTIFACT_CLOSEOUT_INCOMPLETE`. "Will clean
later" is not a completed state.

---

Expand All @@ -139,6 +199,22 @@ leaves the artifact lost.

---

## Mechanical check

`pnpm check:runtime-artifact-location` scans the L1 templates, skills, workflows, and
framework code for writes that may place artifacts in a temp directory. It recognizes
the Node write APIs, `apply_patch` `Add/Update File` targets, shell redirection,
`tee`, `cp`, `mv`, and common `--output` arguments. A form whose lifecycle cannot be
proved fails closed; the author must choose an explicit `.agent/` destination or add a
documented exception.

When a host exposes a Stop event, use it to inventory the run's temp paths and record
`deleted`, `promoted`, or `owned_by_live_process` for each one. If the host has no
blocking Stop gate, the same closeout remains a task-completion requirement and the
result must be reported explicitly.

---

## Cross-references

- .agent/rules/ai-debug-artifacts.md — the dedicated .agent/debug/ space for
Expand Down
2 changes: 1 addition & 1 deletion templates/en/.agent/rules/core-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ status: stable
1. **Adhere to Language Settings**: Always communicate in the language defined in `.agent/rules/language.md`.
2. **Architecture First**: Always check `.agent/rules/architecture-design.md` before suggesting any code changes.
3. **Behavior Rules**: Read `.agent/rules/ai-behavior.md` at session start — it governs Git discipline, edit scope, code exploration strategy (including Graphify-first when `graphify-out/graph.json` exists), and staged-commit protocol.
4. **Runtime Artifact Location**: Anything that must persist across the session — per-run JSON, per-sample observations, debug logs, scratch JSON, probe scripts — goes under `.agent/`, per `.agent/rules/runtime-artifacts-location.md`. **Never** write to `/tmp`, the home directory, or the project root: those are reclaimed with the session and the artifact is lost.
4. **Runtime Artifact Location**: Choose the location by lifetime. JSON, logs, screenshots, and scripts that another command, reviewer, handoff, or resumed task will read belong under `.agent/`. Only process-local scratch created and verified deleted within the same top-level command may use the OS temp directory. See `.agent/rules/runtime-artifacts-location.md`.
5. **Memory Index**: At session start the SessionStart hook auto-loads `.agent/memory/MEMORY.md` (≤200 lines / 25KB cap); read topic files (`{user,feedback,project,reference}/*.md`) on demand. See `.agent/rules/memory-protocol.md` for details.
6. **Proposal Structure**: Read `.agent/rules/proposal-structure.md` before creating or reorganizing architecture proposals, especially for large projects or related-project proposal groups.
7. **Follow Workflows**: When a user runs a command (e.g., `/start-task`), strictly follow the corresponding workflow defined in `.agent/workflows/`.
Expand Down
Loading
Loading