Skip to content

feat(workflow): record team revision - #1201

Open
ousamabenyounes wants to merge 1 commit into
Tencent:mainfrom
ousamabenyounes:feat/issue-1163
Open

ousamabenyounes wants to merge 1 commit into
Tencent:mainfrom
ousamabenyounes:feat/issue-1163

Conversation

@ousamabenyounes

Copy link
Copy Markdown
Contributor

Summary

Workflow run records currently expose only a rendered-source hash, so a reviewer cannot tie a run to the exact shared team commit that supplied it. Capture the team origin commit only when it stores the exact cached workflow and prompt bytes, propagate it when the whole workflow tree agrees, and publish it beside sourceHash in start/end records.

Keep legacy records readable while documenting the provenance fields in English and Chinese.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes
  • npx vitest run passes in exact-head fork CI; the local host's baseline-identical failures are documented below
  • Added/updated tests for the change

Test verification (RED → GREEN)

On unmodified upstream, the targeted behavioral expectations fail for missing cache/run provenance. For example:

 FAIL  src/__tests__/workflow-records.test.ts > #1083 workflow run records > builds a start record with exactly the allowlisted fields
AssertionError: expected undefined to be 'abc' // Object.is equality

The two negative-origin cases each include a positive origin control. On unmodified upstream, those controls fail first because provenance does not exist yet:

 FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits the team revision for an untracked workflow at a remote-reachable commit
AssertionError: expected undefined to be '06337c2d3050e601c901374185281d74364c4…' // Object.is equality
 FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits a commit pushed only to a remote other than the team origin
AssertionError: expected undefined to be 'dd2c34786cb5b57b96aa0020fe72e37c3d941…' // Object.is equality

Making the reader type require current provenance also proves the legacy compatibility problem:

src/__tests__/workflow-records.test.ts(139,11): error TS2741: Property 'sourceHash' is missing in type '{ schemaVersion: "v1alpha1"; run: string; workflow: string; phase: "start"; status: "running"; startedAt: string; username: string; publishedAt: string; }' but required in type 'WorkflowRunRecord'.
TypeScript: 1 errors in 1 files

After the implementation, the complete focused files pass, including the
cross-platform symlink-alias regression:

 Test Files  2 passed (2)
      Tests  199 passed (199)

The first exact-head fork CI exposed a macOS-only /var → /private/var
canonical-path mismatch. The new regression failed before the fix:

FAIL src/__tests__/workflow-cache.test.ts > workflow cache > stores the team revision when the knowledge path is a symlink alias
AssertionError: expected undefined to be '<team commit>'

Canonicalizing the Git top level and source path makes that test pass. Removing
only those two realpath calls returns the same assertion to RED; restoring
them returns all 169 workflow-cache tests to GREEN.

The changed reports-branch E2E passes separately:

 ✓ src/__tests__/e2e/workflow-records-reports-branch-1083.test.ts (1 test) 1927ms
   ✓ #1083 workflow run records reach the reports branch > publishes start and end records plus stats totals to origin  1922ms
 Test Files  1 passed (1)
      Tests  1 passed (1)

Mutation checks restore the exact final production files, make the reader require current provenance, and return an empty legacy queue. Restoring the four production files on the final test commit returns every provenance expectation to RED; the output identifies the failing assertion and location for each case:

FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > stores only a remote-reachable commit that holds the cached workflow bytes
AssertionError: expected undefined to be 'e242b37be1cfa9fe6608110c9a3e7f6156405…' // Object.is equality
 ❯ src/__tests__/workflow-cache.test.ts:98:71
FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits the team revision for an untracked workflow at a remote-reachable commit
AssertionError: expected undefined to be '6639d34420389195e0cc24499b0c9e2b012bd…' // Object.is equality
 ❯ src/__tests__/workflow-cache.test.ts:122:75
FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits a commit pushed only to a remote other than the team origin
AssertionError: expected undefined to be '0112be5a7402622b8d1ba9d7a89d76fc823f3…' // Object.is equality
 ❯ src/__tests__/workflow-cache.test.ts:138:71
FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits the team revision when a prompt file differs from the remote-reachable commit
AssertionError: expected undefined to be '5389ecfb9ca97c5a59d025dec21467562b5ae…' // Object.is equality
 ❯ src/__tests__/workflow-cache.test.ts:169:71
FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > publishes no run team revision when a kept parent and current child came from different commits
AssertionError: expected undefined to be '3b3da53244e7a12e680aafd99d505e7bb800b…' // Object.is equality
 ❯ src/__tests__/workflow-cache.test.ts:201:35
FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > clears a kept workflow revision that the team remote no longer reaches
AssertionError: expected undefined to be defined
 ❯ src/__tests__/workflow-cache.test.ts:240:71
FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > clears an unreadable kept workflow revision
AssertionError: expected 'not-a-commit' to be undefined
 ❯ src/__tests__/workflow-cache.test.ts:273:71
FAIL  src/__tests__/workflow-records.test.ts > #1083 workflow run records > builds a start record with exactly the allowlisted fields
AssertionError: expected undefined to be 'abc' // Object.is equality
 ❯ src/__tests__/workflow-records.test.ts:120:31
FAIL  src/__tests__/workflow-records.test.ts > #1083 workflow run records > reads and merges queued records published before and after source provenance
AssertionError: expected undefined to be 'abc' // Object.is equality
 ❯ src/__tests__/workflow-records.test.ts:158:32

 Test Files  2 failed (2)
      Tests  9 failed | 189 passed (198)

A selective mutant that accepts any checked-out commit and skips source-byte comparison reaches the two negative assertions themselves:

 FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits the team revision for an untracked workflow at a remote-reachable commit
AssertionError: expected 'b22e698544ce1ed8c69eaa9d2c94d07c2cfd3…' to be undefined
 ❯ src/__tests__/workflow-cache.test.ts:128:71
 FAIL  src/__tests__/workflow-cache.test.ts > workflow cache > omits a commit pushed only to a remote other than the team origin
AssertionError: expected 'e1bf9c0f8a583d7eabc97430d0ee2aa2057eb…' to be undefined
 ❯ src/__tests__/workflow-cache.test.ts:150:71
 Test Files  1 failed (1)
      Tests  2 failed | 166 skipped (168)

The strict-reader mutation fails compilation:

src/__tests__/workflow-records.test.ts(139,11): error TS2741: Property 'sourceHash' is missing in type '{ schemaVersion: "v1alpha1"; run: string; workflow: string; phase: "start"; status: "running"; startedAt: string; username: string; publishedAt: string; }' but required in type 'WorkflowRunRecord'.
TypeScript: 1 errors in 1 files

The empty-reader mutation fails the real queue assertion:

FAIL src/__tests__/workflow-records.test.ts > reads and merges queued records published before and after source provenance
AssertionError: expected undefined to be defined
Test Files: 1 failed
Tests: 1 failed, 29 skipped

Production diff coverage is complete:

diff-coverage-gate: PASS 100.00% (80/80) changed production lines covered

Exact-head CI for a248c4def0e76d53438f877c54d736c3a933509e is green on
Ubuntu and macOS with Node 20, 22, and 24; the build plus its focused E2E gates
also pass: https://github.com/ousamabenyounes/teamai-cli/actions/runs/39093552069.

Full local suite

run-ci.sh ran on both the unmodified base and final head. Install, lint, typecheck, build, targeted OpenCode recall, and unattended-init gates pass on final. The baseline generic diff gate exits 1 because no baseline coverage file exists; the final generic gate exits 1 because it classifies changed TypeScript test files as production while LCOV omits tests. The production-only final diff result above is 80/80:

install	0
lint	0
typecheck	0
unit-coverage	1
diff-coverage	1
build	0
opencode-recall-e2e	0
init-unattended-e2e	0
fork-e2e	1

Both unit runs include the Vitest runner header. Unit coverage has the same six host-local failures and 144 skips, with nine new passes:

 RUN  v3.2.7 /home/mike911902_gmail_com/.ora/stack-scratch/69821e3cffdf45bea15885d19b03767f/work/teamai-cli/.worktrees/baseline-1163
      Coverage enabled with v8
 Test Files  4 failed | 461 passed | 1 skipped (466)
      Tests  6 failed | 10745 passed | 144 skipped (10895)
 RUN  v3.2.7 /home/mike911902_gmail_com/.ora/stack-scratch/69821e3cffdf45bea15885d19b03767f/work/teamai-cli/.worktrees/issue-1163
      Coverage enabled with v8
 Test Files  4 failed | 461 passed | 1 skipped (466)
      Tests  6 failed | 10754 passed | 144 skipped (10904)

This host initially lacked sqlite3 and had Git 2.43 while the config-hook path requires Git 2.54; supplying isolated SQLite 3.45.1 and Git 2.54 makes five of those six tests pass. The remaining unrelated uninstall assertion reproduces on the unmodified base. Upstream CI for base SHA f58f3154e2d003d2da1fcd7e6e9223cde107f38d is green across the complete Ubuntu/macOS Node 20/22/24 unit matrix: https://github.com/Tencent/teamai-cli/actions/runs/39039041492. The PR CI matrix is the final authority for that host-specific assertion.

Extracting the failed test names from the baseline and final logs yields the same set:

BASELINE
 FAIL  src/__tests__/cursor-bubble-range.test.ts > cursor bubble cleanup > uses an index range and rejects wildcard ids
 FAIL  src/__tests__/cursor-user-images.test.ts > migrating user images to Cursor > writes a placeholder for each user image in the composer bubbles
 FAIL  src/__tests__/cursor-user-images.test.ts > writing reasoning to Cursor > keeps a raw reasoning block as <thinking> text in the transcript, not in the bubbles
 FAIL  src/__tests__/submodule-checkout.test.ts > submodule checkouts > preserves an empty old .claude/settings.local.json without blocking Git hook installation
 FAIL  src/__tests__/submodule-checkout.test.ts > submodule checkouts > preserves an empty old .codex/hooks.json without blocking Git hook installation
 FAIL  src/__tests__/uninstall.test.ts > uninstall > the block protects a config holding a resolved value (#882) > removes the block once uninstall has taken teamai's servers out of .mcp.json
FINAL
 FAIL  src/__tests__/cursor-bubble-range.test.ts > cursor bubble cleanup > uses an index range and rejects wildcard ids
 FAIL  src/__tests__/cursor-user-images.test.ts > migrating user images to Cursor > writes a placeholder for each user image in the composer bubbles
 FAIL  src/__tests__/cursor-user-images.test.ts > writing reasoning to Cursor > keeps a raw reasoning block as <thinking> text in the transcript, not in the bubbles
 FAIL  src/__tests__/submodule-checkout.test.ts > submodule checkouts > preserves an empty old .claude/settings.local.json without blocking Git hook installation
 FAIL  src/__tests__/submodule-checkout.test.ts > submodule checkouts > preserves an empty old .codex/hooks.json without blocking Git hook installation
 FAIL  src/__tests__/uninstall.test.ts > uninstall > the block protects a config holding a resolved value (#882) > removes the block once uninstall has taken teamai's servers out of .mcp.json

Fork E2E retains the same pre-existing submodule-checkout-1041 hook-config failure. Baseline and amended final SHA both have 1,248 passed / 1 failed / 64 skipped. The targeted OpenCode gate also passes.

 Test Files  1 failed | 152 passed | 6 skipped (159)
      Tests  1 failed | 1248 passed | 64 skipped (1313)

Real CLI verification

The built CLI ran teamai pull and teamai workflow start verify --driver local; the run published successfully:

{"data":{"done":{"run":"r68dcc00d2768d7b7","outcome":"succeeded","record":"published"},"cancelled":[]}}

The published record contains the expected provenance:

  "workflow": "verify",
  "sourceHash": "78b2ecd38a15687f033a9dbc2c6d121b2e7a825855e43177a67bd25998cb1d63",
  "teamRevision": "0d6848c87da4e4de4d8d0dc81ccce89081f382ef",
  "phase": "end",

git cat-file -t <teamRevision> returns commit, and hashing git show <teamRevision>:.teamai/workflows/verify.yaml reproduces sourceHash:

commit
78b2ecd38a15687f033a9dbc2c6d121b2e7a825855e43177a67bd25998cb1d63  -

Related Issues

Closes #1163

Notes for Reviewers

The configured team repository is the origin remote, so personal remotes cannot make a local commit publishable. No fetch or other network operation occurs while caching.

teamRevision stays optional for legacy readers. A non-code validation failure may retain an earlier valid cache and its still-reachable revision; coded diagnostics continue to discard that cache under the existing safety rule.

@github-actions

Copy link
Copy Markdown

I can’t complete the exact-scope review: the checkout is shallow, and the requested git diff 58706fe...a248c4d fails with no merge base. Remote access also failed. A two-dot comparison includes unrelated changes, so I won’t substitute it or report findings from it.

The supplied PR description meets the testing-record requirement: it documents a build and representative real-CLI run with published provenance.

Please provide the exact three-dot diff or a checkout containing the missing base history. No PR code was run, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against #1163 and its follow-up comment on the full commit id.

Spec item Covered by Status
1. cat-file -e resolves every published value e2e, in the producing clone (see inline) met
2. git show + hash equals sourceHash e2e, and the real-CLI run in the body met
3. uncommitted or unpushed edit publishes no teamRevision stores only a remote-reachable commit that holds the cached workflow bytes met
4. kept entry keeps its earlier value mixed parent/child test asserts the kept cache met in code; no test runs a kept single workflow and reads the run's value
5. parent and child at different commits give no teamRevision mixed parent/child test met
6. legacy records parse and merge; new fields pass the current reader records test; the reader is JSON.parse plus a spreading mergeRecord met
Comment: full 40-char commit rev-parse HEAD met
Watch-outs: no fetch in the cache step, both guide pages, based on #1148 met

Limiting reachability to refs/remotes/origin/* is narrower than the issue's "a remote-tracking ref". It is the right reading: origin is the team repo, so criterion 1 holds by construction and a personal remote cannot make a commit publishable. Clearing a kept revision that origin no longer reaches goes past the issue text and stays within its intent.

Ran npx vitest run src/__tests__/workflow-cache.test.ts src/__tests__/workflow-records.test.ts on a248c4d: 199 passed.

One non-blocking finding and two nits inline. Approving.

Comment thread src/workflow/cache.ts
Comment on lines +274 to +278
if (options.dryRun || (previous?.sourceHash === file.sourceHash && previous.teamRevision === file.teamRevision)) continue;
await writeJsonAtomic(cachePath(config, file.definition.name), {
relativePath: file.relativePath,
sourceHash: file.sourceHash,
...(file.teamRevision ? { teamRevision: file.teamRevision } : {}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 non-blocking] An unrelated unpushed commit erases a revision that is still valid.

Scenario: build.yaml is pushed at A and cached with teamRevision: A. The member then commits something unrelated and does not push. This is the normal state in self mode, where .teamai/ lives in the project repo and HEAD is usually ahead of origin. On the next pull or push, reachable is undefined, so file.teamRevision is undefined. It differs from previous.teamRevision, so the entry is rewritten without A. The run then publishes no teamRevision, although A still holds those exact bytes and origin still reaches it.

An equal sourceHash means equal bytes. The kept loop above has already removed a previous revision that origin no longer reaches. So the earlier value can be carried forward, which is the issue's rule for kept entries applied to entries confirmed unchanged:

Suggested change
if (options.dryRun || (previous?.sourceHash === file.sourceHash && previous.teamRevision === file.teamRevision)) continue;
await writeJsonAtomic(cachePath(config, file.definition.name), {
relativePath: file.relativePath,
sourceHash: file.sourceHash,
...(file.teamRevision ? { teamRevision: file.teamRevision } : {}),
const teamRevision = file.teamRevision ?? (previous?.sourceHash === file.sourceHash ? previous.teamRevision : undefined);
if (options.dryRun || (previous?.sourceHash === file.sourceHash && previous.teamRevision === teamRevision)) continue;
await writeJsonAtomic(cachePath(config, file.definition.name), {
relativePath: file.relativePath,
sourceHash: file.sourceHash,
...(teamRevision ? { teamRevision } : {}),

Regression test. It fails on a248c4d. With the change it passes, both test files stay green (200 passed), and tsc --noEmit is clean:

it('keeps a verified revision when an unrelated unpushed commit moves HEAD', async () => {
  const config = testConfig();
  const repository = initializeTeamRepository(config);
  writeWorkflow(config, 'build', VALID);
  const pushed = repository.commit('add workflow');
  repository.push();
  await syncWorkflowDefinitions(config);

  fs.writeFileSync(path.join(config.repo.localPath, 'README.md'), 'unrelated\n');
  repository.commit('unrelated local commit');
  await syncWorkflowDefinitions(config);

  expect((await loadCachedWorkflow(config, 'build'))?.teamRevision).toBe(pushed);
});

expect(startRecord.username).toBe('alice');
expect(startRecord.sourceHash).toBe(sourceHash);
expect(startRecord.teamRevision).toBe(teamRevision);
expect(() => git(repo, 'cat-file', '-e', teamRevision)).not.toThrow();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3 nit] Criterion 1 says "in a fresh clone". repo is the clone that produced the value, so cat-file -e passes there whether or not origin has the commit. Checking criteria 1 and 2 against a fresh clone of origin.git proves the claim:

Suggested change
expect(() => git(repo, 'cat-file', '-e', teamRevision)).not.toThrow();
git(sandbox, 'clone', 'origin.git', 'fresh');
const fresh = path.join(sandbox, 'fresh');
expect(() => git(fresh, 'cat-file', '-e', teamRevision)).not.toThrow();
expect(git(fresh, 'show', `${teamRevision}:workflows/build.yaml`)).toBe(TWO_STEPS);

If the real-CLI check in the PR body was also run in the working clone, a fresh clone there would match the issue's test plan.

Comment thread src/workflow/cache.ts
async function remotelyReachableRevision(root: string): Promise<ReachableRevision | undefined> {
try {
const git = createGit(root);
const revision = (await git.revparse(['HEAD'])).trim();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3 nit] The value is already the full commit id, so the issue comment is satisfied. The comment points at getHeadCommit (utils/git.ts), which #1176 and #1180 use for the same purpose. Reusing it keeps one way to resolve the team commit:

const revision = await getHeadCommit(root);
if (!revision || !await isTeamRemoteRevision(root, revision)) return undefined;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Define the team revision once for run records, labels and snapshots

3 participants